Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NIOT #95

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.2
// swift-tools-version:5.3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't change the tools version as we need to keep support for 5.2 - does that block the PR?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe 5.3 is require for the

condition: .when(
                     platforms: [Platform.iOS, Platform.macOS, Platform.tvOS, Platform.watchOS]
                 )

If this is a blocker then i might look into how i could make changes to this package so that an additional package could provide the NIOTransportServices decency. (either through a subclass or a plugin like solution). what do you think would be the best approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a [email protected] manifest which contains the old version and then that should work (we might need to wrap the new methods in a Swift version check as well)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok i will give that a go, should i also update the GitHub workflow so that it includes both versions of swift?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gwynne could you update the CI with all the new fun stuff?

import PackageDescription

let package = Package(
Expand All @@ -12,6 +12,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.11.1"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
],
targets: [
.target(name: "WebSocketKit", dependencies: [
Expand All @@ -21,6 +22,13 @@ let package = Package(
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(
name: "NIOTransportServices",
package: "swift-nio-transport-services",
condition: .when(
platforms: [Platform.iOS, Platform.macOS, Platform.tvOS, Platform.watchOS]
)
)
]),
.testTarget(name: "WebSocketKitTests", dependencies: [
.target(name: "WebSocketKit"),
Expand Down
29 changes: 29 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version:5.2
import PackageDescription

let package = Package(
name: "websocket-kit",
platforms: [
.macOS(.v10_15)
],
products: [
.library(name: "WebSocketKit", targets: ["WebSocketKit"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.11.1"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
],
targets: [
.target(name: "WebSocketKit", dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "NIOWebSocket", package: "swift-nio")
]),
.testTarget(name: "WebSocketKitTests", dependencies: [
.target(name: "WebSocketKit"),
]),
]
)
57 changes: 57 additions & 0 deletions Sources/WebSocketKit/HTTPChannelIntercepter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
import NIOHTTP1
import NIO

public final class HTTPChannelIntercepter: ChannelDuplexHandler, RemovableChannelHandler {

public typealias OutboundIn = HTTPClientRequestPart
public typealias OutboundOut = HTTPClientRequestPart

public typealias InboundIn = HTTPClientResponsePart
public typealias InboundOut = HTTPClientResponsePart

let writeInterceptHandler: (HTTPRequestHead) -> Void

public init(writeInterceptHandler: @escaping (HTTPRequestHead) -> Void) {
self.writeInterceptHandler = writeInterceptHandler
}

public func write(
context: ChannelHandlerContext,
data: NIOAny,
promise: EventLoopPromise<Void>?
) {
let interceptedOutgoingRequest = self.unwrapOutboundIn(data)

if case .head(let requestHead) = interceptedOutgoingRequest {
self.writeInterceptHandler(requestHead)
}

context.write(data, promise: promise)
}
}

extension ChannelPipeline {
public func addHTTPClientHandlers(
position: Position = .last,
leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes,
withServerUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil,
withExtraHandlers extraHandlers: [RemovableChannelHandler] = []
) -> EventLoopFuture<Void> {
let requestEncoder = HTTPRequestEncoder()
let responseDecoder = HTTPResponseDecoder(leftOverBytesStrategy: leftOverBytesStrategy)

var handlers: [RemovableChannelHandler] = [requestEncoder, ByteToMessageHandler(responseDecoder)] + extraHandlers

if let (upgraders, completionHandler) = upgrade {
let upgrader = NIOHTTPClientUpgradeHandler(
upgraders: upgraders,
httpHandlers: handlers,
upgradeCompletionHandler: completionHandler
)
handlers.append(upgrader)
}

return self.addHandlers(handlers, position: position)
}
}
141 changes: 141 additions & 0 deletions Sources/WebSocketKit/TLSConfiguration+Network.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//===----------------------------------------------------------------------===//
//
// This source file was part of the AsyncHTTPClient open source project
// https://github.com/swift-server/async-http-client
//
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Network) && swift(>=5.3)

import Foundation
import Network
import NIOSSL
import NIOTransportServices

extension TLSVersion {
/// return Network framework TLS protocol version
var nwTLSProtocolVersion: tls_protocol_version_t {
switch self {
case .tlsv1:
return .TLSv10
case .tlsv11:
return .TLSv11
case .tlsv12:
return .TLSv12
case .tlsv13:
return .TLSv13
}
}
}

extension TLSVersion {
/// return as SSL protocol
var sslProtocol: SSLProtocol {
switch self {
case .tlsv1:
return .tlsProtocol1
case .tlsv11:
return .tlsProtocol11
case .tlsv12:
return .tlsProtocol12
case .tlsv13:
return .tlsProtocol13
}
}
}

@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension TLSConfiguration {
/// Dispatch queue used by Network framework TLS to control certificate verification
static var tlsDispatchQueue = DispatchQueue(label: "TLSDispatch")

/// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration
///
/// - Parameter queue: Dispatch queue to run `sec_protocol_options_set_verify_block` on.
/// - Returns: Equivalent NWProtocolTLS Options
func getNWProtocolTLSOptions() -> NWProtocolTLS.Options {
let options = NWProtocolTLS.Options()

// minimum TLS protocol
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion.nwTLSProtocolVersion)
} else {
sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol)
}

// maximum TLS protocol
if let maximumTLSVersion = self.maximumTLSVersion {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion.nwTLSProtocolVersion)
} else {
sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol)
}
}

// application protocols
for applicationProtocol in self.applicationProtocols {
applicationProtocol.withCString { buffer in
sec_protocol_options_add_tls_application_protocol(options.securityProtocolOptions, buffer)
}
}

// the certificate chain
if self.certificateChain.count > 0 {
preconditionFailure("TLSConfiguration.certificateChain is not supported")
}

// cipher suites
if self.cipherSuites.count > 0 {
// TODO: Requires NIOSSL to provide list of cipher values before we can continue
// https://github.com/apple/swift-nio-ssl/issues/207
}

// key log callback
if self.keyLogCallback != nil {
preconditionFailure("TLSConfiguration.keyLogCallback is not supported")
}

// private key
if self.privateKey != nil {
preconditionFailure("TLSConfiguration.privateKey is not supported")
}

// renegotiation support key is unsupported

// trust roots
if let trustRoots = self.trustRoots {
guard case .default = trustRoots else {
preconditionFailure("TLSConfiguration.trustRoots != .default is not supported")
}
}

switch self.certificateVerification {
case .none:
// add verify block to control certificate verification
sec_protocol_options_set_verify_block(
options.securityProtocolOptions,
{ _, _, sec_protocol_verify_complete in
sec_protocol_verify_complete(true)
}, TLSConfiguration.tlsDispatchQueue
)

case .noHostnameVerification:
precondition(self.certificateVerification != .noHostnameVerification, "TLSConfiguration.certificateVerification = .noHostnameVerification is not supported")

case .fullVerification:
break
}

return options
}
}

#endif
59 changes: 56 additions & 3 deletions Sources/WebSocketKit/WebSocket+Connect.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import NIOHTTP1

extension WebSocket {
public static func connect(
to url: String,
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
onUpgrade: @escaping (WebSocket, HTTPResponseHead) -> ()
) -> EventLoopFuture<Void> {
guard let url = URL(string: url) else {
return eventLoopGroup.next().makeFailedFuture(WebSocketClient.Error.invalidURL)
Expand All @@ -17,13 +19,28 @@ extension WebSocket {
onUpgrade: onUpgrade
)
}

public static func connect(
to url: String,
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
return self.connect(
to: url,
headers: headers,
configuration: configuration,
on: eventLoopGroup
) { (ws, _ ) in onUpgrade(ws) }
}

public static func connect(
to url: URL,
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
onUpgrade: @escaping (WebSocket, HTTPResponseHead) -> ()
) -> EventLoopFuture<Void> {
let scheme = url.scheme ?? "ws"
return self.connect(
Expand All @@ -37,6 +54,21 @@ extension WebSocket {
onUpgrade: onUpgrade
)
}

public static func connect(
to url: URL,
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
return self.connect(
to: url,
headers: headers,
configuration: configuration,
on: eventLoopGroup
) { (ws, _) in onUpgrade(ws) }
}

public static func connect(
scheme: String = "ws",
Expand All @@ -46,7 +78,7 @@ extension WebSocket {
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
onUpgrade: @escaping (WebSocket, HTTPResponseHead) -> ()
) -> EventLoopFuture<Void> {
return WebSocketClient(
eventLoopGroupProvider: .shared(eventLoopGroup),
Expand All @@ -60,4 +92,25 @@ extension WebSocket {
onUpgrade: onUpgrade
)
}

public static func connect(
scheme: String = "ws",
host: String,
port: Int = 80,
path: String = "/",
headers: HTTPHeaders = [:],
configuration: WebSocketClient.Configuration = .init(),
on eventLoopGroup: EventLoopGroup,
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
return self.connect(
scheme: scheme,
host: host,
port: port,
path: path,
headers: headers,
configuration: configuration,
on: eventLoopGroup
) { (ws, _) in onUpgrade(ws) }
}
}
Loading