mirror of
https://github.com/apple/swift-nio-extras.git
synced 2025-06-01 10:36:15 +08:00
Add HTTP types adapter for SwiftNIO
This commit is contained in:
parent
fb70a0f5e9
commit
2e8f942098
.spi.ymlPackage.swiftREADME.md
Sources
NIOHTTPTypes
NIOHTTPTypesHTTP1
NIOHTTPTypesHTTP2
Tests
NIOHTTPTypesHTTP1Tests
NIOHTTPTypesHTTP2Tests
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]
|
||||
|
@ -123,6 +123,34 @@ var targets: [PackageDescription.Target] = [
|
||||
.product(name: "NIOEmbedded", package: "swift-nio"),
|
||||
.product(name: "NIOTestUtils", package: "swift-nio"),
|
||||
]),
|
||||
.target(
|
||||
name: "NIOHTTPTypes",
|
||||
dependencies: [
|
||||
.product(name: "HTTPTypes", package: "swift-http-types"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
]),
|
||||
.target(
|
||||
name: "NIOHTTPTypesHTTP1",
|
||||
dependencies: [
|
||||
"NIOHTTPTypes",
|
||||
.product(name: "NIOHTTP1", package: "swift-nio"),
|
||||
]),
|
||||
.target(
|
||||
name: "NIOHTTPTypesHTTP2",
|
||||
dependencies: [
|
||||
"NIOHTTPTypes",
|
||||
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
|
||||
]),
|
||||
.testTarget(
|
||||
name: "NIOHTTPTypesHTTP1Tests",
|
||||
dependencies: [
|
||||
"NIOHTTPTypesHTTP1",
|
||||
]),
|
||||
.testTarget(
|
||||
name: "NIOHTTPTypesHTTP2Tests",
|
||||
dependencies: [
|
||||
"NIOHTTPTypesHTTP2",
|
||||
]),
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
@ -131,10 +159,15 @@ let package = Package(
|
||||
.library(name: "NIOExtras", targets: ["NIOExtras"]),
|
||||
.library(name: "NIOSOCKS", targets: ["NIOSOCKS"]),
|
||||
.library(name: "NIOHTTPCompression", targets: ["NIOHTTPCompression"]),
|
||||
.library(name: "NIOHTTPTypes", targets: ["NIOHTTPTypes"]),
|
||||
.library(name: "NIOHTTPTypesHTTP1", targets: ["NIOHTTPTypesHTTP1"]),
|
||||
.library(name: "NIOHTTPTypesHTTP2", targets: ["NIOHTTPTypesHTTP2"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.27.0"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-http-types", from: "0.1.0"),
|
||||
],
|
||||
targets: targets
|
||||
)
|
||||
|
@ -50,3 +50,9 @@ On the [`nio-extras-0.1`](https://github.com/apple/swift-nio-extras/tree/nio-ext
|
||||
- [`DebugInboundsEventHandler`](Sources/NIOExtras/DebugInboundEventsHandler.swift) Prints out all inbound events that travel through the `ChannelPipeline`.
|
||||
- [`DebugOutboundsEventHandler`](Sources/NIOExtras/DebugOutboundEventsHandler.swift) Prints out all outbound events that travel through the `ChannelPipeline`.
|
||||
- [`WritePCAPHandler`](Sources/NIOExtras/WritePCAPHandler.swift) A `ChannelHandler` that writes `.pcap` containing the traffic of the `ChannelPipeline` that you can inspect with Wireshark/tcpdump.
|
||||
- [`HTTP1ToHTTPClientCodec`](Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/1 messages into shared HTTP types for the client side.
|
||||
- [`HTTP1ToHTTPServerCodec`](Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/1 messages into shared HTTP types for the server side.
|
||||
- [`HTTPToHTTP1ClientCodec`](Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift) A `ChannelHandler` that translates shared HTTP types into HTTP/1 messages for the client side for compatibility purposes.
|
||||
- [`HTTPToHTTP1ServerCodec`](Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift) A `ChannelHandler` that translates shared HTTP types into HTTP/1 messages for the server side for compatibility purposes.
|
||||
- [`HTTP2FramePayloadToHTTPClientCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the client side.
|
||||
- [`HTTP2FramePayloadToHTTPServerCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the server side.
|
||||
|
44
Sources/NIOHTTPTypes/NIOHTTPTypes.swift
Normal file
44
Sources/NIOHTTPTypes/NIOHTTPTypes.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIOCore
|
||||
import HTTPTypes
|
||||
|
||||
/// The parts of a complete HTTP message, either request or response.
|
||||
///
|
||||
/// An HTTP message is made up of a request, or one or more response headers,
|
||||
/// encoded by `.head`, zero or more body parts, and optionally some trailers. To
|
||||
/// indicate that a complete HTTP message has been sent or received, we use `.end`,
|
||||
/// which may also contain any trailers that make up the message.
|
||||
public enum HTTPTypePart<HeadT: Equatable, BodyT: Equatable> {
|
||||
case head(HeadT)
|
||||
case body(BodyT)
|
||||
case end(HTTPFields?)
|
||||
}
|
||||
|
||||
extension HTTPTypePart: Sendable where HeadT: Sendable, BodyT: Sendable {}
|
||||
|
||||
extension HTTPTypePart: Equatable {}
|
||||
|
||||
/// The components of an HTTP request from the view of an HTTP client.
|
||||
public typealias HTTPTypeClientRequestPart = HTTPTypePart<HTTPRequest, IOData>
|
||||
|
||||
/// The components of an HTTP request from the view of an HTTP server.
|
||||
public typealias HTTPTypeServerRequestPart = HTTPTypePart<HTTPRequest, ByteBuffer>
|
||||
|
||||
/// The components of an HTTP response from the view of an HTTP client.
|
||||
public typealias HTTPTypeClientResponsePart = HTTPTypePart<HTTPResponse, ByteBuffer>
|
||||
|
||||
/// The components of an HTTP response from the view of an HTTP server.
|
||||
public typealias HTTPTypeServerResponsePart = HTTPTypePart<HTTPResponse, IOData>
|
110
Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift
Normal file
110
Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift
Normal file
@ -0,0 +1,110 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIOCore
|
||||
import NIOHTTP1
|
||||
import NIOHTTPTypes
|
||||
|
||||
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types,
|
||||
/// and vice versa, for use on the client side.
|
||||
public final class HTTP1ToHTTPClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTPClientResponsePart
|
||||
public typealias InboundOut = HTTPTypeClientResponsePart
|
||||
|
||||
public typealias OutboundIn = HTTPTypeClientRequestPart
|
||||
public typealias OutboundOut = HTTPClientRequestPart
|
||||
|
||||
/// Initializes a `HTTP1ToHTTPClientCodec`.
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
switch unwrapInboundIn(data) {
|
||||
case .head(let head):
|
||||
do {
|
||||
context.fireChannelRead(wrapInboundOut(.head(try head.newResponse)))
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.fireChannelRead(wrapInboundOut(.body(body)))
|
||||
case .end(let trailers):
|
||||
context.fireChannelRead(wrapInboundOut(.end(trailers?.newFields(splitCookie: false))))
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
switch unwrapOutboundIn(data) {
|
||||
case .head(let request):
|
||||
do {
|
||||
context.write(wrapOutboundOut(.head(try HTTPRequestHead(request))), promise: promise)
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.write(wrapOutboundOut(.body(body)), promise: promise)
|
||||
case .end(let trailers):
|
||||
context.write(wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types,
|
||||
/// and vice versa, for use on the server side.
|
||||
public final class HTTP1ToHTTPServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTPServerRequestPart
|
||||
public typealias InboundOut = HTTPTypeServerRequestPart
|
||||
|
||||
public typealias OutboundIn = HTTPTypeServerResponsePart
|
||||
public typealias OutboundOut = HTTPServerResponsePart
|
||||
|
||||
private let secure: Bool
|
||||
private let splitCookie: Bool
|
||||
|
||||
/// Initializes a `HTTP1ToHTTPServerCodec`.
|
||||
/// - Parameters:
|
||||
/// - secure: Whether "https" or "http" is used.
|
||||
/// - splitCookie: Whether the cookies received from the server should be split
|
||||
/// into multiple header fields. Defaults to false.
|
||||
public init(secure: Bool, splitCookie: Bool = false) {
|
||||
self.secure = secure
|
||||
self.splitCookie = splitCookie
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
switch unwrapInboundIn(data) {
|
||||
case .head(let head):
|
||||
do {
|
||||
context.fireChannelRead(wrapInboundOut(.head(try head.newRequest(secure: secure, splitCookie: splitCookie))))
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.fireChannelRead(wrapInboundOut(.body(body)))
|
||||
case .end(let trailers):
|
||||
context.fireChannelRead(wrapInboundOut(.end(trailers?.newFields(splitCookie: false))))
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
switch unwrapOutboundIn(data) {
|
||||
case .head(let response):
|
||||
context.write(wrapOutboundOut(.head(HTTPResponseHead(response))), promise: promise)
|
||||
case .body(let body):
|
||||
context.write(wrapOutboundOut(.body(body)), promise: promise)
|
||||
case .end(let trailers):
|
||||
context.write(wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise)
|
||||
}
|
||||
}
|
||||
}
|
118
Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift
Normal file
118
Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.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 NIOCore
|
||||
import NIOHTTP1
|
||||
import NIOHTTPTypes
|
||||
|
||||
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages,
|
||||
/// and vice versa, for use on the client side.
|
||||
///
|
||||
/// This is intended for compatibility purposes where a channel handler working with
|
||||
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types
|
||||
/// abstraction.
|
||||
public final class HTTPToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTPTypeClientResponsePart
|
||||
public typealias InboundOut = HTTPClientResponsePart
|
||||
|
||||
public typealias OutboundIn = HTTPClientRequestPart
|
||||
public typealias OutboundOut = HTTPTypeClientRequestPart
|
||||
|
||||
private let secure: Bool
|
||||
private let splitCookie: Bool
|
||||
|
||||
/// Initializes a `HTTPToHTTP1ClientCodec`.
|
||||
/// - Parameters:
|
||||
/// - secure: Whether "https" or "http" is used.
|
||||
/// - splitCookie: Whether the cookies sent by the client should be split
|
||||
/// into multiple header fields. Defaults to true.
|
||||
public init(secure: Bool, splitCookie: Bool = true) {
|
||||
self.secure = secure
|
||||
self.splitCookie = splitCookie
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
switch unwrapInboundIn(data) {
|
||||
case .head(let head):
|
||||
context.fireChannelRead(wrapInboundOut(.head(HTTPResponseHead(head))))
|
||||
case .body(let body):
|
||||
context.fireChannelRead(wrapInboundOut(.body(body)))
|
||||
case .end(let trailers):
|
||||
context.fireChannelRead(wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
switch unwrapOutboundIn(data) {
|
||||
case .head(let request):
|
||||
do {
|
||||
context.write(wrapOutboundOut(.head(try request.newRequest(secure: secure, splitCookie: splitCookie))), promise: promise)
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.write(wrapOutboundOut(.body(body)), promise: promise)
|
||||
case .end(let trailers):
|
||||
context.write(wrapOutboundOut(.end(trailers?.newFields(splitCookie: false))), promise: promise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages,
|
||||
/// and vice versa, for use on the server side.
|
||||
///
|
||||
/// This is intended for compatibility purposes where a channel handler working with
|
||||
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types
|
||||
/// abstraction.
|
||||
public final class HTTPToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTPTypeServerRequestPart
|
||||
public typealias InboundOut = HTTPServerRequestPart
|
||||
|
||||
public typealias OutboundIn = HTTPServerResponsePart
|
||||
public typealias OutboundOut = HTTPTypeServerResponsePart
|
||||
|
||||
/// Initializes a `HTTPToHTTP1ServerCodec`.
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
switch unwrapInboundIn(data) {
|
||||
case .head(let head):
|
||||
do {
|
||||
context.fireChannelRead(wrapInboundOut(.head(try HTTPRequestHead(head))))
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.fireChannelRead(wrapInboundOut(.body(body)))
|
||||
case .end(let trailers):
|
||||
context.fireChannelRead(wrapInboundOut(.end(trailers.map(HTTPHeaders.init))))
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
switch unwrapOutboundIn(data) {
|
||||
case .head(let response):
|
||||
do {
|
||||
context.write(wrapOutboundOut(.head(try response.newResponse)), promise: promise)
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
case .body(let body):
|
||||
context.write(wrapOutboundOut(.body(body)), promise: promise)
|
||||
case .end(let trailers):
|
||||
context.write(wrapOutboundOut(.end(trailers?.newFields(splitCookie: false))), promise: promise)
|
||||
}
|
||||
}
|
||||
}
|
195
Sources/NIOHTTPTypesHTTP1/HTTPTypeConversion.swift
Normal file
195
Sources/NIOHTTPTypesHTTP1/HTTPTypeConversion.swift
Normal file
@ -0,0 +1,195 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIOHTTP1
|
||||
import HTTPTypes
|
||||
|
||||
private enum HTTP1TypeConversionError: Error {
|
||||
case invalidMethod
|
||||
case missingPath
|
||||
case invalidStatusCode
|
||||
}
|
||||
|
||||
extension HTTPMethod {
|
||||
init(_ newMethod: HTTPRequest.Method) {
|
||||
switch newMethod {
|
||||
case .get: self = .GET
|
||||
case .put: self = .PUT
|
||||
case .acl: self = .ACL
|
||||
case .head: self = .HEAD
|
||||
case .post: self = .POST
|
||||
case .copy: self = .COPY
|
||||
case .lock: self = .LOCK
|
||||
case .move: self = .MOVE
|
||||
case .bind: self = .BIND
|
||||
case .link: self = .LINK
|
||||
case .patch: self = .PATCH
|
||||
case .trace: self = .TRACE
|
||||
case .mkcol: self = .MKCOL
|
||||
case .merge: self = .MERGE
|
||||
case .purge: self = .PURGE
|
||||
case .notify: self = .NOTIFY
|
||||
case .search: self = .SEARCH
|
||||
case .unlock: self = .UNLOCK
|
||||
case .rebind: self = .REBIND
|
||||
case .unbind: self = .UNBIND
|
||||
case .report: self = .REPORT
|
||||
case .delete: self = .DELETE
|
||||
case .unlink: self = .UNLINK
|
||||
case .connect: self = .CONNECT
|
||||
case .msearch: self = .MSEARCH
|
||||
case .options: self = .OPTIONS
|
||||
case .propfind: self = .PROPFIND
|
||||
case .checkout: self = .CHECKOUT
|
||||
case .proppatch: self = .PROPPATCH
|
||||
case .subscribe: self = .SUBSCRIBE
|
||||
case .mkcalendar: self = .MKCALENDAR
|
||||
case .mkactivity: self = .MKACTIVITY
|
||||
case .unsubscribe: self = .UNSUBSCRIBE
|
||||
case .source: self = .SOURCE
|
||||
default: self = .RAW(value: newMethod.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var newMethod: HTTPRequest.Method {
|
||||
get throws {
|
||||
switch self {
|
||||
case .GET: return .get
|
||||
case .PUT: return .put
|
||||
case .ACL: return .acl
|
||||
case .HEAD: return .head
|
||||
case .POST: return .post
|
||||
case .COPY: return .copy
|
||||
case .LOCK: return .lock
|
||||
case .MOVE: return .move
|
||||
case .BIND: return .bind
|
||||
case .LINK: return .link
|
||||
case .PATCH: return .patch
|
||||
case .TRACE: return .trace
|
||||
case .MKCOL: return .mkcol
|
||||
case .MERGE: return .merge
|
||||
case .PURGE: return .purge
|
||||
case .NOTIFY: return .notify
|
||||
case .SEARCH: return .search
|
||||
case .UNLOCK: return .unlock
|
||||
case .REBIND: return .rebind
|
||||
case .UNBIND: return .unbind
|
||||
case .REPORT: return .report
|
||||
case .DELETE: return .delete
|
||||
case .UNLINK: return .unlink
|
||||
case .CONNECT: return .connect
|
||||
case .MSEARCH: return .msearch
|
||||
case .OPTIONS: return .options
|
||||
case .PROPFIND: return .propfind
|
||||
case .CHECKOUT: return .checkout
|
||||
case .PROPPATCH: return .proppatch
|
||||
case .SUBSCRIBE: return .subscribe
|
||||
case .MKCALENDAR: return .mkcalendar
|
||||
case .MKACTIVITY: return .mkactivity
|
||||
case .UNSUBSCRIBE: return .unsubscribe
|
||||
case .SOURCE: return .source
|
||||
case .RAW(value: let value):
|
||||
guard let method = HTTPRequest.Method(value) else {
|
||||
throw HTTP1TypeConversionError.invalidMethod
|
||||
}
|
||||
return method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPHeaders {
|
||||
init(_ newFields: HTTPFields) {
|
||||
let fields = newFields.map { ($0.name.rawName, $0.value) }
|
||||
self.init(fields)
|
||||
}
|
||||
|
||||
func newFields(splitCookie: Bool) -> HTTPFields {
|
||||
var fields = HTTPFields()
|
||||
fields.reserveCapacity(count)
|
||||
for (index, field) in enumerated() {
|
||||
if index == 0 && field.name.lowercased() == "host" {
|
||||
continue
|
||||
}
|
||||
if let name = HTTPField.Name(field.name) {
|
||||
if splitCookie && name == .cookie, #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
|
||||
fields.append(contentsOf: field.value.split(separator: "; ", omittingEmptySubsequences: false).map {
|
||||
HTTPField(name: name, value: String($0))
|
||||
})
|
||||
} else {
|
||||
fields.append(HTTPField(name: name, value: field.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPRequestHead {
|
||||
init(_ newRequest: HTTPRequest) throws {
|
||||
guard let pathField = newRequest.pseudoHeaderFields.path else {
|
||||
throw HTTP1TypeConversionError.missingPath
|
||||
}
|
||||
var headers = HTTPHeaders()
|
||||
headers.reserveCapacity(newRequest.headerFields.count + 1)
|
||||
if let authorityField = newRequest.pseudoHeaderFields.authority {
|
||||
headers.add(name: "Host", value: authorityField.value)
|
||||
}
|
||||
var firstCookie = true
|
||||
for field in newRequest.headerFields {
|
||||
if field.name == .cookie {
|
||||
if firstCookie {
|
||||
firstCookie = false
|
||||
headers.add(name: field.name.rawName, value: newRequest.headerFields[.cookie]!)
|
||||
}
|
||||
} else {
|
||||
headers.add(name: field.name.rawName, value: field.value)
|
||||
}
|
||||
}
|
||||
self.init(version: .http1_1,
|
||||
method: HTTPMethod(newRequest.method),
|
||||
uri: pathField.value,
|
||||
headers: headers)
|
||||
}
|
||||
|
||||
func newRequest(secure: Bool, splitCookie: Bool) throws -> HTTPRequest {
|
||||
let method = try method.newMethod
|
||||
let scheme = secure ? "https" : "http"
|
||||
let authority = headers.first.flatMap { $0.name.lowercased() == "host" ? $0.value : nil }
|
||||
return HTTPRequest(method: method,
|
||||
scheme: scheme,
|
||||
authority: authority,
|
||||
path: uri,
|
||||
headerFields: headers.newFields(splitCookie: splitCookie))
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPResponseHead {
|
||||
init(_ newResponse: HTTPResponse) {
|
||||
self.init(version: .http1_1,
|
||||
status: HTTPResponseStatus(statusCode: newResponse.status.code,
|
||||
reasonPhrase: newResponse.status.reasonPhrase),
|
||||
headers: HTTPHeaders(newResponse.headerFields))
|
||||
}
|
||||
|
||||
var newResponse: HTTPResponse {
|
||||
get throws {
|
||||
guard status.code <= 999 else {
|
||||
throw HTTP1TypeConversionError.invalidStatusCode
|
||||
}
|
||||
let status = HTTPResponse.Status(code: Int(status.code), reasonPhrase: status.reasonPhrase)
|
||||
return HTTPResponse(status: status, headerFields: headers.newFields(splitCookie: false))
|
||||
}
|
||||
}
|
||||
}
|
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 NIOHTTP2
|
||||
import NIOHPACK
|
||||
|
||||
extension HPACKHeaders {
|
||||
/// Whether this `HTTPHeaders` corresponds to a final response or not.
|
||||
///
|
||||
/// This function is only valid if called on a response header block. If the :status header
|
||||
/// is not present, this will throw.
|
||||
fileprivate func isInformationalResponse() throws -> Bool {
|
||||
return try self.peekPseudoHeader(name: ":status").first! == "1"
|
||||
}
|
||||
|
||||
/// Grabs a pseudo-header from a header block. Does not remove it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The header name to find.
|
||||
/// - Returns: The value for this pseudo-header.
|
||||
/// - Throws: `NIOHTTP2Errors` if there is no such header, or multiple.
|
||||
internal func peekPseudoHeader(name: String) throws -> String {
|
||||
// This could be done with .lazy.filter.map but that generates way more ARC traffic.
|
||||
var headerValue: String? = nil
|
||||
|
||||
for (fieldName, fieldValue, _) in self {
|
||||
if name == fieldName {
|
||||
guard headerValue == nil else {
|
||||
throw NIOHTTP2Errors.duplicatePseudoHeader(name)
|
||||
}
|
||||
headerValue = fieldValue
|
||||
}
|
||||
}
|
||||
|
||||
if let headerValue = headerValue {
|
||||
return headerValue
|
||||
} else {
|
||||
throw NIOHTTP2Errors.missingPseudoHeader(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that keeps track of the header blocks sent or received and that determines the type of any
|
||||
/// new header block.
|
||||
struct HTTP2HeadersStateMachine {
|
||||
/// The list of possible header frame types.
|
||||
///
|
||||
/// This is used in combination with introspection of the HTTP header blocks to determine what HTTP header block
|
||||
/// a certain HTTP header is.
|
||||
enum HeaderType {
|
||||
/// A request header block.
|
||||
case requestHead
|
||||
|
||||
/// An informational response header block. These can be sent zero or more times.
|
||||
case informationalResponseHead
|
||||
|
||||
/// A final response header block.
|
||||
case finalResponseHead
|
||||
|
||||
/// A trailer block. Once this is sent no further header blocks are acceptable.
|
||||
case trailer
|
||||
}
|
||||
|
||||
/// The previous header block.
|
||||
private var previousHeader: HeaderType?
|
||||
|
||||
/// The mode of this connection: client or server.
|
||||
private let mode: NIOHTTP2Handler.ParserMode
|
||||
|
||||
init(mode: NIOHTTP2Handler.ParserMode) {
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
/// Called when about to process an HTTP headers block to determine its type.
|
||||
mutating func newHeaders(block: HPACKHeaders) throws -> HeaderType {
|
||||
let newType: HeaderType
|
||||
|
||||
switch (self.mode, self.previousHeader) {
|
||||
case (.server, .none):
|
||||
// The first header block received on a server mode stream must be a request block.
|
||||
newType = .requestHead
|
||||
case (.client, .none),
|
||||
(.client, .some(.informationalResponseHead)):
|
||||
// The first header block received on a client mode stream may be either informational or final,
|
||||
// depending on the value of the :status pseudo-header. Alternatively, if the previous
|
||||
// header block was informational, the same possibilities apply.
|
||||
newType = try block.isInformationalResponse() ? .informationalResponseHead : .finalResponseHead
|
||||
case (.server, .some(.requestHead)),
|
||||
(.client, .some(.finalResponseHead)):
|
||||
// If the server has already received a request head, or the client has already received a final response,
|
||||
// this is a trailer block.
|
||||
newType = .trailer
|
||||
case (.server, .some(.informationalResponseHead)),
|
||||
(.server, .some(.finalResponseHead)),
|
||||
(.client, .some(.requestHead)):
|
||||
// These states should not be reachable!
|
||||
preconditionFailure("Invalid internal state!")
|
||||
case (.server, .some(.trailer)),
|
||||
(.client, .some(.trailer)):
|
||||
// TODO(cory): This should probably throw, as this can happen in malformed programs without the world ending.
|
||||
preconditionFailure("Sending too many header blocks.")
|
||||
}
|
||||
|
||||
self.previousHeader = newType
|
||||
return newType
|
||||
}
|
||||
}
|
244
Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift
Normal file
244
Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift
Normal file
@ -0,0 +1,244 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIOCore
|
||||
import NIOHTTP2
|
||||
import NIOHPACK
|
||||
import HTTPTypes
|
||||
import NIOHTTPTypes
|
||||
|
||||
// MARK: - Client
|
||||
|
||||
private struct BaseClientCodec {
|
||||
private var headerStateMachine: HTTP2HeadersStateMachine = HTTP2HeadersStateMachine(mode: .client)
|
||||
|
||||
private var outgoingHTTP1RequestHead: HTTPRequest?
|
||||
|
||||
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPTypeClientResponsePart?, second: HTTPTypeClientResponsePart?) {
|
||||
switch data {
|
||||
case .headers(let headerContent):
|
||||
switch try self.headerStateMachine.newHeaders(block: headerContent.headers) {
|
||||
case .trailer:
|
||||
return (first: .end(try headerContent.headers.newTrailers), second: nil)
|
||||
|
||||
case .informationalResponseHead:
|
||||
return (first: .head(try headerContent.headers.newResponse), second: nil)
|
||||
|
||||
case .finalResponseHead:
|
||||
guard outgoingHTTP1RequestHead != nil else {
|
||||
preconditionFailure("Expected not to get a response without having sent a request")
|
||||
}
|
||||
self.outgoingHTTP1RequestHead = nil
|
||||
let respHead = try headerContent.headers.newResponse
|
||||
let first = HTTPTypeClientResponsePart.head(respHead)
|
||||
var second: HTTPTypeClientResponsePart? = nil
|
||||
if headerContent.endStream {
|
||||
second = .end(nil)
|
||||
}
|
||||
return (first: first, second: second)
|
||||
|
||||
case .requestHead:
|
||||
preconditionFailure("A client can not receive request heads")
|
||||
}
|
||||
case .data(let content):
|
||||
guard case .byteBuffer(let b) = content.data else {
|
||||
preconditionFailure("Received DATA frame with non-bytebuffer IOData")
|
||||
}
|
||||
|
||||
var first = HTTPTypeClientResponsePart.body(b)
|
||||
var second: HTTPTypeClientResponsePart? = nil
|
||||
if content.endStream {
|
||||
if b.readableBytes == 0 {
|
||||
first = .end(nil)
|
||||
} else {
|
||||
second = .end(nil)
|
||||
}
|
||||
}
|
||||
return (first: first, second: second)
|
||||
case .alternativeService, .rstStream, .priority, .windowUpdate, .settings, .pushPromise, .ping, .goAway, .origin:
|
||||
// These are not meaningful in HTTP messaging, so drop them.
|
||||
return (first: nil, second: nil)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func processOutboundData(_ data: HTTPTypeClientRequestPart, allocator: ByteBufferAllocator) throws -> HTTP2Frame.FramePayload {
|
||||
switch data {
|
||||
case .head(let head):
|
||||
precondition(self.outgoingHTTP1RequestHead == nil, "Only a single HTTP request allowed per HTTP2 stream")
|
||||
self.outgoingHTTP1RequestHead = head
|
||||
let headerContent = HTTP2Frame.FramePayload.Headers(headers: HPACKHeaders(head))
|
||||
return .headers(headerContent)
|
||||
case .body(let body):
|
||||
return .data(HTTP2Frame.FramePayload.Data(data: body))
|
||||
case .end(let trailers):
|
||||
if let trailers = trailers {
|
||||
return .headers(.init(headers: HPACKHeaders(trailers),
|
||||
endStream: true))
|
||||
} else {
|
||||
return .data(.init(data: .byteBuffer(allocator.buffer(capacity: 0)), endStream: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple channel handler that translates HTTP/2 concepts into shared HTTP types,
|
||||
/// and vice versa, for use on the client side.
|
||||
///
|
||||
/// Use this channel handler alongside the `HTTP2StreamMultiplexer` to
|
||||
/// help provide an HTTP transaction-level abstraction on top of an HTTP/2 multiplexed
|
||||
/// connection.
|
||||
///
|
||||
/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type.
|
||||
public final class HTTP2FramePayloadToHTTPClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTP2Frame.FramePayload
|
||||
public typealias InboundOut = HTTPTypeClientResponsePart
|
||||
|
||||
public typealias OutboundIn = HTTPTypeClientRequestPart
|
||||
public typealias OutboundOut = HTTP2Frame.FramePayload
|
||||
|
||||
private var baseCodec: BaseClientCodec = .init()
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
let payload = self.unwrapInboundIn(data)
|
||||
do {
|
||||
let (first, second) = try self.baseCodec.processInboundData(payload)
|
||||
if let first = first {
|
||||
context.fireChannelRead(self.wrapInboundOut(first))
|
||||
}
|
||||
if let second = second {
|
||||
context.fireChannelRead(self.wrapInboundOut(second))
|
||||
}
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
let requestPart = self.unwrapOutboundIn(data)
|
||||
|
||||
do {
|
||||
let transformedPayload = try self.baseCodec.processOutboundData(requestPart, allocator: context.channel.allocator)
|
||||
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
|
||||
} catch {
|
||||
promise?.fail(error)
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Server
|
||||
|
||||
private struct BaseServerCodec {
|
||||
private var headerStateMachine: HTTP2HeadersStateMachine = HTTP2HeadersStateMachine(mode: .server)
|
||||
|
||||
mutating func processInboundData(_ data: HTTP2Frame.FramePayload) throws -> (first: HTTPTypeServerRequestPart?, second: HTTPTypeServerRequestPart?) {
|
||||
switch data {
|
||||
case .headers(let headerContent):
|
||||
if case .trailer = try self.headerStateMachine.newHeaders(block: headerContent.headers) {
|
||||
return (first: .end(try headerContent.headers.newTrailers), second: nil)
|
||||
} else {
|
||||
let reqHead = try headerContent.headers.newRequest
|
||||
|
||||
let first = HTTPTypeServerRequestPart.head(reqHead)
|
||||
var second: HTTPTypeServerRequestPart? = nil
|
||||
if headerContent.endStream {
|
||||
second = .end(nil)
|
||||
}
|
||||
return (first: first, second: second)
|
||||
}
|
||||
case .data(let dataContent):
|
||||
guard case .byteBuffer(let b) = dataContent.data else {
|
||||
preconditionFailure("Received non-byteBuffer IOData from network")
|
||||
}
|
||||
var first = HTTPTypeServerRequestPart.body(b)
|
||||
var second: HTTPTypeServerRequestPart? = nil
|
||||
if dataContent.endStream {
|
||||
if b.readableBytes == 0 {
|
||||
first = .end(nil)
|
||||
} else {
|
||||
second = .end(nil)
|
||||
}
|
||||
}
|
||||
return (first: first, second: second)
|
||||
default:
|
||||
// Any other frame type is ignored.
|
||||
return (first: nil, second: nil)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func processOutboundData(_ data: HTTPTypeServerResponsePart, allocator: ByteBufferAllocator) -> HTTP2Frame.FramePayload {
|
||||
switch data {
|
||||
case .head(let head):
|
||||
let payload = HTTP2Frame.FramePayload.Headers(headers: HPACKHeaders(head))
|
||||
return .headers(payload)
|
||||
case .body(let body):
|
||||
let payload = HTTP2Frame.FramePayload.Data(data: body)
|
||||
return .data(payload)
|
||||
case .end(let trailers):
|
||||
if let trailers = trailers {
|
||||
return .headers(.init(headers: HPACKHeaders(trailers),
|
||||
endStream: true))
|
||||
} else {
|
||||
return .data(.init(data: .byteBuffer(allocator.buffer(capacity: 0)), endStream: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A simple channel handler that translates HTTP/2 concepts into shared HTTP types,
|
||||
/// and vice versa, for use on the server side.
|
||||
///
|
||||
/// Use this channel handler alongside the `HTTP2StreamMultiplexer` to
|
||||
/// help provide an HTTP transaction-level abstraction on top of an HTTP/2 multiplexed
|
||||
/// connection.
|
||||
///
|
||||
/// This handler uses `HTTP2Frame.FramePayload` as its HTTP/2 currency type.
|
||||
public final class HTTP2FramePayloadToHTTPServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
|
||||
public typealias InboundIn = HTTP2Frame.FramePayload
|
||||
public typealias InboundOut = HTTPTypeServerRequestPart
|
||||
|
||||
public typealias OutboundIn = HTTPTypeServerResponsePart
|
||||
public typealias OutboundOut = HTTP2Frame.FramePayload
|
||||
|
||||
private var baseCodec: BaseServerCodec = .init()
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
let payload = self.unwrapInboundIn(data)
|
||||
|
||||
do {
|
||||
let (first, second) = try self.baseCodec.processInboundData(payload)
|
||||
if let first = first {
|
||||
context.fireChannelRead(self.wrapInboundOut(first))
|
||||
}
|
||||
if let second = second {
|
||||
context.fireChannelRead(self.wrapInboundOut(second))
|
||||
}
|
||||
} catch {
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
||||
let responsePart = self.unwrapOutboundIn(data)
|
||||
let transformedPayload = self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator)
|
||||
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
|
||||
}
|
||||
}
|
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 NIOHPACK
|
||||
import HTTPTypes
|
||||
|
||||
private enum HTTP2TypeConversionError: Error {
|
||||
case multipleMethod
|
||||
case multipleScheme
|
||||
case multipleAuthority
|
||||
case multiplePath
|
||||
case multipleProtocol
|
||||
case missingMethod
|
||||
case invalidMethod
|
||||
|
||||
case multipleStatus
|
||||
case missingStatus
|
||||
case invalidStatus
|
||||
|
||||
case pseudoFieldNotFirst
|
||||
case pseudoFieldInTrailers
|
||||
}
|
||||
|
||||
extension HPACKIndexing {
|
||||
init(_ newIndexingStrategy: HTTPField.DynamicTableIndexingStrategy) {
|
||||
switch newIndexingStrategy {
|
||||
case .avoid: self = .nonIndexable
|
||||
case .disallow: self = .neverIndexed
|
||||
default: self = .indexable
|
||||
}
|
||||
}
|
||||
|
||||
var newIndexingStrategy: HTTPField.DynamicTableIndexingStrategy {
|
||||
switch self {
|
||||
case .indexable: return .automatic
|
||||
case .nonIndexable: return .avoid
|
||||
case .neverIndexed: return .disallow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HPACKHeaders {
|
||||
mutating func add(newField field: HTTPField) {
|
||||
add(name: field.name.canonicalName, value: field.value, indexing: HPACKIndexing(field.indexingStrategy))
|
||||
}
|
||||
|
||||
init(_ newRequest: HTTPRequest) {
|
||||
var headers = HPACKHeaders()
|
||||
headers.reserveCapacity(newRequest.headerFields.count + 5)
|
||||
|
||||
headers.add(newField: newRequest.pseudoHeaderFields.method)
|
||||
if let field = newRequest.pseudoHeaderFields.scheme {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
if let field = newRequest.pseudoHeaderFields.authority {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
if let field = newRequest.pseudoHeaderFields.path {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
if let field = newRequest.pseudoHeaderFields.extendedConnectProtocol {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
for field in newRequest.headerFields {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
self = headers
|
||||
}
|
||||
|
||||
init(_ newResponse: HTTPResponse) {
|
||||
var headers = HPACKHeaders()
|
||||
headers.reserveCapacity(newResponse.headerFields.count + 1)
|
||||
|
||||
headers.add(newField: newResponse.pseudoHeaderFields.status)
|
||||
for field in newResponse.headerFields {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
self = headers
|
||||
}
|
||||
|
||||
init(_ newTrailers: HTTPFields) {
|
||||
var headers = HPACKHeaders()
|
||||
headers.reserveCapacity(newTrailers.count)
|
||||
for field in newTrailers {
|
||||
headers.add(newField: field)
|
||||
}
|
||||
self = headers
|
||||
}
|
||||
|
||||
var newRequest: HTTPRequest {
|
||||
get throws {
|
||||
var methodString: String? = nil
|
||||
var methodIndexable: HPACKIndexing = .indexable
|
||||
var schemeString: String? = nil
|
||||
var schemeIndexable: HPACKIndexing = .indexable
|
||||
var authorityString: String? = nil
|
||||
var authorityIndexable: HPACKIndexing = .indexable
|
||||
var pathString: String? = nil
|
||||
var pathIndexable: HPACKIndexing = .indexable
|
||||
var protocolString: String? = nil
|
||||
var protocolIndexable: HPACKIndexing = .indexable
|
||||
|
||||
var i = startIndex
|
||||
while i != endIndex {
|
||||
let (name, value, indexable) = self[i]
|
||||
if !name.hasPrefix(":") {
|
||||
break
|
||||
}
|
||||
switch name {
|
||||
case ":method":
|
||||
if methodString != nil {
|
||||
throw HTTP2TypeConversionError.multipleMethod
|
||||
}
|
||||
methodString = value
|
||||
methodIndexable = indexable
|
||||
case ":scheme":
|
||||
if schemeString != nil {
|
||||
throw HTTP2TypeConversionError.multipleScheme
|
||||
}
|
||||
schemeString = value
|
||||
schemeIndexable = indexable
|
||||
case ":authority":
|
||||
if authorityString != nil {
|
||||
throw HTTP2TypeConversionError.multipleAuthority
|
||||
}
|
||||
authorityString = value
|
||||
authorityIndexable = indexable
|
||||
case ":path":
|
||||
if pathString != nil {
|
||||
throw HTTP2TypeConversionError.multiplePath
|
||||
}
|
||||
pathString = value
|
||||
pathIndexable = indexable
|
||||
case ":protocol":
|
||||
if protocolString != nil {
|
||||
throw HTTP2TypeConversionError.multipleProtocol
|
||||
}
|
||||
protocolString = value
|
||||
protocolIndexable = indexable
|
||||
default:
|
||||
continue
|
||||
}
|
||||
i = index(after: i)
|
||||
}
|
||||
|
||||
guard let methodString else {
|
||||
throw HTTP2TypeConversionError.missingMethod
|
||||
}
|
||||
guard let method = HTTPRequest.Method(methodString) else {
|
||||
throw HTTP2TypeConversionError.invalidMethod
|
||||
}
|
||||
|
||||
var request = HTTPRequest(method: method,
|
||||
scheme: schemeString,
|
||||
authority: authorityString,
|
||||
path: pathString)
|
||||
request.pseudoHeaderFields.method.indexingStrategy = methodIndexable.newIndexingStrategy
|
||||
request.pseudoHeaderFields.scheme?.indexingStrategy = schemeIndexable.newIndexingStrategy
|
||||
request.pseudoHeaderFields.authority?.indexingStrategy = authorityIndexable.newIndexingStrategy
|
||||
request.pseudoHeaderFields.path?.indexingStrategy = pathIndexable.newIndexingStrategy
|
||||
if let protocolString {
|
||||
request.extendedConnectProtocol = protocolString
|
||||
request.pseudoHeaderFields.extendedConnectProtocol?.indexingStrategy = protocolIndexable.newIndexingStrategy
|
||||
}
|
||||
|
||||
request.headerFields.reserveCapacity(count)
|
||||
while i != endIndex {
|
||||
let (name, value, indexable) = self[i]
|
||||
if name.hasPrefix(":") {
|
||||
throw HTTP2TypeConversionError.pseudoFieldNotFirst
|
||||
}
|
||||
if let fieldName = HTTPField.Name(name) {
|
||||
var field = HTTPField(name: fieldName, value: value)
|
||||
field.indexingStrategy = indexable.newIndexingStrategy
|
||||
request.headerFields.append(field)
|
||||
}
|
||||
i = index(after: i)
|
||||
}
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
var newResponse: HTTPResponse {
|
||||
get throws {
|
||||
var statusString: String? = nil
|
||||
var statusIndexable: HPACKIndexing = .indexable
|
||||
|
||||
var i = startIndex
|
||||
while i != endIndex {
|
||||
let (name, value, indexable) = self[i]
|
||||
if !name.hasPrefix(":") {
|
||||
break
|
||||
}
|
||||
switch name {
|
||||
case ":status":
|
||||
if statusString != nil {
|
||||
throw HTTP2TypeConversionError.multipleStatus
|
||||
}
|
||||
statusString = value
|
||||
statusIndexable = indexable
|
||||
default:
|
||||
continue
|
||||
}
|
||||
i = index(after: i)
|
||||
}
|
||||
|
||||
guard let statusString else {
|
||||
throw HTTP2TypeConversionError.missingStatus
|
||||
}
|
||||
guard let status = Int(statusString),
|
||||
(0...999).contains(status) else {
|
||||
throw HTTP2TypeConversionError.invalidStatus
|
||||
}
|
||||
|
||||
var response = HTTPResponse(status: HTTPResponse.Status(code: status))
|
||||
response.pseudoHeaderFields.status.indexingStrategy = statusIndexable.newIndexingStrategy
|
||||
|
||||
response.headerFields.reserveCapacity(count)
|
||||
while i != endIndex {
|
||||
let (name, value, indexable) = self[i]
|
||||
if name.hasPrefix(":") {
|
||||
throw HTTP2TypeConversionError.pseudoFieldNotFirst
|
||||
}
|
||||
if let fieldName = HTTPField.Name(name) {
|
||||
var field = HTTPField(name: fieldName, value: value)
|
||||
field.indexingStrategy = indexable.newIndexingStrategy
|
||||
response.headerFields.append(field)
|
||||
}
|
||||
i = index(after: i)
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
var newTrailers: HTTPFields {
|
||||
get throws {
|
||||
var fields = HTTPFields()
|
||||
fields.reserveCapacity(count)
|
||||
for (name, value, indexable) in self {
|
||||
if name.hasPrefix(":") {
|
||||
throw HTTP2TypeConversionError.pseudoFieldInTrailers
|
||||
}
|
||||
if let fieldName = HTTPField.Name(name) {
|
||||
var field = HTTPField(name: fieldName, value: value)
|
||||
field.indexingStrategy = indexable.newIndexingStrategy
|
||||
fields.append(field)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
}
|
||||
}
|
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 XCTest
|
||||
import NIOCore
|
||||
import NIOEmbedded
|
||||
import NIOHTTP1
|
||||
import HTTPTypes
|
||||
import NIOHTTPTypes
|
||||
import NIOHTTPTypesHTTP1
|
||||
|
||||
/// A handler that keeps track of all reads made on a channel.
|
||||
private final class InboundRecorder<Frame>: ChannelInboundHandler {
|
||||
typealias InboundIn = Frame
|
||||
|
||||
var receivedFrames: [Frame] = []
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
self.receivedFrames.append(self.unwrapInboundIn(data))
|
||||
}
|
||||
}
|
||||
|
||||
private extension HTTPField.Name {
|
||||
static let xFoo = Self("X-Foo")!
|
||||
}
|
||||
|
||||
final class NIOHTTPTypesHTTP1Tests: XCTestCase {
|
||||
var channel: EmbeddedChannel!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.channel = EmbeddedChannel()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
self.channel = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
static let request = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
|
||||
.accept: "*/*",
|
||||
.acceptEncoding: "gzip",
|
||||
.acceptEncoding: "br",
|
||||
.cookie: "a=b",
|
||||
.cookie: "c=d",
|
||||
.trailer: "X-Foo",
|
||||
])
|
||||
|
||||
static let requestNoSplitCookie = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
|
||||
.accept: "*/*",
|
||||
.acceptEncoding: "gzip",
|
||||
.acceptEncoding: "br",
|
||||
.cookie: "a=b; c=d",
|
||||
.trailer: "X-Foo",
|
||||
])
|
||||
|
||||
static let oldRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: [
|
||||
"Host": "www.example.com",
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Accept-Encoding": "br",
|
||||
"Cookie": "a=b; c=d",
|
||||
"Trailer": "X-Foo",
|
||||
])
|
||||
|
||||
static let response = HTTPResponse(status: .ok, headerFields: [
|
||||
.server: "HTTPServer/1.0",
|
||||
.trailer: "X-Foo",
|
||||
])
|
||||
|
||||
static let oldResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: [
|
||||
"Server": "HTTPServer/1.0",
|
||||
"Trailer": "X-Foo",
|
||||
])
|
||||
|
||||
static let trailers: HTTPFields = [.xFoo: "Bar"]
|
||||
|
||||
static let oldTrailers: HTTPHeaders = ["X-Foo": "Bar"]
|
||||
|
||||
func testClientHTTP1ToHTTP() throws {
|
||||
let recorder = InboundRecorder<HTTPTypeClientResponsePart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTP1ToHTTPClientCodec(), recorder).wait()
|
||||
|
||||
try self.channel.writeOutbound(HTTPTypeClientRequestPart.head(Self.request))
|
||||
try self.channel.writeOutbound(HTTPTypeClientRequestPart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPClientRequestPart.self), .head(Self.oldRequest))
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPClientRequestPart.self), .end(Self.oldTrailers))
|
||||
|
||||
try self.channel.writeInbound(HTTPClientResponsePart.head(Self.oldResponse))
|
||||
try self.channel.writeInbound(HTTPClientResponsePart.end(Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.response))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
|
||||
func testServerHTTP1ToHTTP() throws {
|
||||
let recorder = InboundRecorder<HTTPTypeServerRequestPart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTP1ToHTTPServerCodec(secure: true), recorder).wait()
|
||||
|
||||
try self.channel.writeInbound(HTTPServerRequestPart.head(Self.oldRequest))
|
||||
try self.channel.writeInbound(HTTPServerRequestPart.end(Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.requestNoSplitCookie))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
|
||||
|
||||
try self.channel.writeOutbound(HTTPTypeServerResponsePart.head(Self.response))
|
||||
try self.channel.writeOutbound(HTTPTypeServerResponsePart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPServerResponsePart.self), .head(Self.oldResponse))
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPServerResponsePart.self), .end(Self.oldTrailers))
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
|
||||
func testClientHTTPToHTTP1() throws {
|
||||
let recorder = InboundRecorder<HTTPClientResponsePart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTPToHTTP1ClientCodec(secure: true), recorder).wait()
|
||||
|
||||
try self.channel.writeOutbound(HTTPClientRequestPart.head(Self.oldRequest))
|
||||
try self.channel.writeOutbound(HTTPClientRequestPart.end(Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeClientRequestPart.self), .head(Self.request))
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeClientRequestPart.self), .end(Self.trailers))
|
||||
|
||||
try self.channel.writeInbound(HTTPTypeClientResponsePart.head(Self.response))
|
||||
try self.channel.writeInbound(HTTPTypeClientResponsePart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.oldResponse))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.oldTrailers))
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
|
||||
func testServerHTTPToHTTP1() throws {
|
||||
let recorder = InboundRecorder<HTTPServerRequestPart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTPToHTTP1ServerCodec(), recorder).wait()
|
||||
|
||||
try self.channel.writeInbound(HTTPTypeServerRequestPart.head(Self.request))
|
||||
try self.channel.writeInbound(HTTPTypeServerRequestPart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.oldRequest))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.oldTrailers))
|
||||
|
||||
try self.channel.writeOutbound(HTTPServerResponsePart.head(Self.oldResponse))
|
||||
try self.channel.writeOutbound(HTTPServerResponsePart.end(Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeServerResponsePart.self), .head(Self.response))
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTPTypeServerResponsePart.self), .end(Self.trailers))
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
}
|
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 XCTest
|
||||
import NIOCore
|
||||
import NIOEmbedded
|
||||
import NIOHTTP2
|
||||
import NIOHPACK
|
||||
import HTTPTypes
|
||||
import NIOHTTPTypes
|
||||
import NIOHTTPTypesHTTP2
|
||||
|
||||
/// A handler that keeps track of all reads made on a channel.
|
||||
private final class InboundRecorder<Frame>: ChannelInboundHandler {
|
||||
typealias InboundIn = Frame
|
||||
|
||||
var receivedFrames: [Frame] = []
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
self.receivedFrames.append(self.unwrapInboundIn(data))
|
||||
}
|
||||
}
|
||||
|
||||
private extension HTTPField.Name {
|
||||
static let xFoo = Self("X-Foo")!
|
||||
}
|
||||
|
||||
private extension HTTP2Frame.FramePayload {
|
||||
var headers: HPACKHeaders? {
|
||||
if case .headers(let headers) = self {
|
||||
return headers.headers
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(headers: HPACKHeaders) {
|
||||
self = .headers(.init(headers: headers))
|
||||
}
|
||||
}
|
||||
|
||||
final class NIOHTTPTypesHTTP2Tests: XCTestCase {
|
||||
var channel: EmbeddedChannel!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.channel = EmbeddedChannel()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
self.channel = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
static let request = HTTPRequest(method: .get, scheme: "https", authority: "www.example.com", path: "/", headerFields: [
|
||||
.accept: "*/*",
|
||||
.acceptEncoding: "gzip",
|
||||
.acceptEncoding: "br",
|
||||
.trailer: "X-Foo",
|
||||
.cookie: "a=b",
|
||||
.cookie: "c=d",
|
||||
])
|
||||
|
||||
static let oldRequest: HPACKHeaders = [
|
||||
":method": "GET",
|
||||
":scheme": "https",
|
||||
":authority": "www.example.com",
|
||||
":path": "/",
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip",
|
||||
"accept-encoding": "br",
|
||||
"trailer": "X-Foo",
|
||||
"cookie": "a=b",
|
||||
"cookie": "c=d",
|
||||
]
|
||||
|
||||
static let response = HTTPResponse(status: .ok, headerFields: [
|
||||
.server: "HTTPServer/1.0",
|
||||
.trailer: "X-Foo",
|
||||
])
|
||||
|
||||
static let oldResponse: HPACKHeaders = [
|
||||
":status": "200",
|
||||
"server": "HTTPServer/1.0",
|
||||
"trailer": "X-Foo",
|
||||
]
|
||||
|
||||
static let trailers: HTTPFields = [.xFoo: "Bar"]
|
||||
|
||||
static let oldTrailers: HPACKHeaders = ["x-foo": "Bar"]
|
||||
|
||||
func testClientHTTP2ToHTTP() throws {
|
||||
let recorder = InboundRecorder<HTTPTypeClientResponsePart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTP2FramePayloadToHTTPClientCodec(), recorder).wait()
|
||||
|
||||
try self.channel.writeOutbound(HTTPTypeClientRequestPart.head(Self.request))
|
||||
try self.channel.writeOutbound(HTTPTypeClientRequestPart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldRequest)
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldTrailers)
|
||||
|
||||
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldResponse))
|
||||
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.response))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
|
||||
func testServerHTTP2ToHTTP() throws {
|
||||
let recorder = InboundRecorder<HTTPTypeServerRequestPart>()
|
||||
|
||||
try self.channel.pipeline.addHandlers(HTTP2FramePayloadToHTTPServerCodec(), recorder).wait()
|
||||
|
||||
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldRequest))
|
||||
try self.channel.writeInbound(HTTP2Frame.FramePayload(headers: Self.oldTrailers))
|
||||
|
||||
XCTAssertEqual(recorder.receivedFrames[0], .head(Self.request))
|
||||
XCTAssertEqual(recorder.receivedFrames[1], .end(Self.trailers))
|
||||
|
||||
try self.channel.writeOutbound(HTTPTypeServerResponsePart.head(Self.response))
|
||||
try self.channel.writeOutbound(HTTPTypeServerResponsePart.end(Self.trailers))
|
||||
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldResponse)
|
||||
XCTAssertEqual(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)?.headers, Self.oldTrailers)
|
||||
|
||||
XCTAssertTrue(try channel.finish().isClean)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user