Skip to content

Commit

Permalink
Add HTTP types adapter for SwiftNIO (#202)
Browse files Browse the repository at this point in the history
* Add HTTP types adapter for SwiftNIO

* swiftformat

* Guard on Swift 5.8

* Review comments

* Update swift-http-types to 0.1.1

* Update swift-http-types to 1.0.0

* Review feedback

* Review feedback

* Bump minimum Swift version to 5.7.1

* Allow Host in any order
  • Loading branch information
guoye-zhang authored Oct 20, 2023
1 parent 6c3819c commit 798c962
Show file tree
Hide file tree
Showing 12 changed files with 1,489 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .spi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS]
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2]
35 changes: 34 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:5.7.1
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
Expand Down Expand Up @@ -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(
Expand All @@ -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
)
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand All @@ -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
--------------------|----------------------
Expand All @@ -34,7 +34,7 @@ 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 ...` | 5.7.1

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.

Expand All @@ -51,3 +51,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.
42 changes: 42 additions & 0 deletions Sources/NIOHTTPTypes/NIOHTTPTypes.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 117 additions & 0 deletions Sources/NIOHTTPTypesHTTP1/HTTP1ToHTTPCodec.swift
Original file line number Diff line number Diff line change
@@ -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 {
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 {
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 changes: 134 additions & 0 deletions Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift
Original file line number Diff line number Diff line change
@@ -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 {
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 {
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)
}
}
}
Loading

0 comments on commit 798c962

Please sign in to comment.