Skip to content

Commit

Permalink
Update for new NIOSSL (#132)
Browse files Browse the repository at this point in the history
* Bump to Swift 5.6 minimum and update README
* The usual CI cleanup
* Require a more recent NIO, switch to NIOCore instead of NIO, use EventLoopGroup.any() instead of .next(), make EventLoopGroupProvider an alias for NIO's version, use annotated exports
* Ditch unneeded Concurrency checks (which also incidentally enables back-deployment to 10.15)
* Update SSLTestHelpers to reflect the current state of the original upstream (which basically just means using OpaquePointer instead of UMP<EVP_PKEY>, partly because EVP_PKEY is no longer visible from CNIOBoringSSL
* Add API breakage allowlist for this branch
* Add test coverage for all the connect methods, binary frames, and ping intervals.
  • Loading branch information
gwynne authored May 15, 2023
1 parent 2166cbe commit 17c0afb
Show file tree
Hide file tree
Showing 17 changed files with 627 additions and 471 deletions.
3 changes: 3 additions & 0 deletions .api-breakage/allowlist-branch-update-for-new-niossl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
API breakage: import NIO has been renamed to import NIOCore
API breakage: import NIO has been renamed to import NIOPosix

9 changes: 0 additions & 9 deletions .github/workflows/main-codecov.yml

This file was deleted.

28 changes: 4 additions & 24 deletions .github/workflows/projectboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,7 @@ on:
types: [reopened, closed, labeled, unlabeled, assigned, unassigned]

jobs:
setup_matrix_input:
runs-on: ubuntu-latest

steps:
- id: set-matrix
run: |
output=$(curl ${{ github.event.issue.url }}/labels | jq '.[] | .name') || output=""
echo '======================'
echo 'Process incoming data'
echo '======================'
json=$(echo $output | sed 's/"\s"/","/g')
echo $json
echo "::set-output name=matrix::$(echo $json)"
outputs:
issueTags: ${{ steps.set-matrix.outputs.matrix }}

Manage_project_issues:
needs: setup_matrix_input
uses: vapor/ci/.github/workflows/issues-to-project-board.yml@main
with:
labelsJson: ${{ needs.setup_matrix_input.outputs.issueTags }}
secrets:
PROJECT_BOARD_AUTOMATION_PAT: "${{ secrets.PROJECT_BOARD_AUTOMATION_PAT }}"
update_project_boards:
name: Update project boards
uses: vapor/ci/.github/workflows/update-project-boards-for-issue.yml@reusable-workflows
secrets: inherit
17 changes: 10 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
name: test
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
- pull_request
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }

jobs:

vapor-integration:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container:
image: swift:5.7
container: swift:5.8-jammy
steps:
- name: Check out package
uses: actions/checkout@v3
Expand All @@ -14,8 +20,7 @@ jobs:
uses: actions/checkout@v3
with: { repository: 'vapor/vapor', path: 'vapor' }
- name: Use local package in Vapor
run: |
swift package --package-path vapor edit websocket-kit --path websocket-kit
run: swift package --package-path vapor edit websocket-kit --path websocket-kit
- name: Run Vapor tests
run: swift test --package-path vapor

Expand All @@ -24,6 +29,4 @@ jobs:
with:
with_coverage: true
with_tsan: false
coverage_ignores: '/Tests/'
with_public_api_check: ${{ github.event_name == 'pull_request' }}

8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
// swift-tools-version:5.4
// swift-tools-version:5.6
import PackageDescription

let package = Package(
name: "websocket-kit",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.watchOS(.v6),
.tvOS(.v13),
],
products: [
.library(name: "WebSocketKit", targets: ["WebSocketKit"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.33.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.53.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.16.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.24.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.4"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
],
targets: [
.target(name: "WebSocketKit", dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIOExtras", package: "swift-nio-extras"),
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
<a href="LICENSE">
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
</a>
<a href="https://github.com/vapor/websocket-kit/actions">
<img src="https://github.com/vapor/websocket-kit/workflows/test/badge.svg" alt="Continuous Integration">
<a href="https://github.com/vapor/websocket-kit/actions/workflows/test.yml">
<img src="https://github.com/vapor/websocket-kit/actions/workflows/test.yml/badge.svg?event=push" alt="Continuous Integration">
</a>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/swift-5.2-brightgreen.svg" alt="Swift 5.2">
<img src="http://img.shields.io/badge/swift-5.6-brightgreen.svg" alt="Swift 5.6">
</a>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/swift-5.8-brightgreen.svg" alt="Swift 5.8">
</a>
</p>
4 changes: 0 additions & 4 deletions Sources/WebSocketKit/Concurrency/WebSocket+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore
import NIOWebSocket
import Foundation
import NIOHTTP1

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
extension WebSocket {
public func send<S>(_ text: S) async throws
where S: Collection, S.Element == Character
Expand Down Expand Up @@ -142,5 +140,3 @@ extension WebSocket {
).get()
}
}

#endif
29 changes: 21 additions & 8 deletions Sources/WebSocketKit/Exports.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
#if !BUILDING_DOCC
#if swift(>=5.8)

@_exported import struct NIO.ByteBuffer
@_exported import protocol NIO.Channel
@_exported import protocol NIO.EventLoop
@_exported import protocol NIO.EventLoopGroup
@_exported import struct NIO.EventLoopPromise
@_exported import class NIO.EventLoopFuture
@_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer
@_documentation(visibility: internal) @_exported import protocol NIOCore.Channel
@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop
@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup
@_documentation(visibility: internal) @_exported import struct NIOCore.EventLoopPromise
@_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture

@_documentation(visibility: internal) @_exported import struct NIOHTTP1.HTTPHeaders

@_documentation(visibility: internal) @_exported import struct Foundation.URL

#else

@_exported import struct NIOCore.ByteBuffer
@_exported import protocol NIOCore.Channel
@_exported import protocol NIOCore.EventLoop
@_exported import protocol NIOCore.EventLoopGroup
@_exported import struct NIOCore.EventLoopPromise
@_exported import class NIOCore.EventLoopFuture

@_exported import struct NIOHTTP1.HTTPHeaders

@_exported import struct Foundation.URL

#endif
#endif
2 changes: 1 addition & 1 deletion Sources/WebSocketKit/HTTPUpgradeRequestHandler.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NIO
import NIOCore
import NIOHTTP1

final class HTTPUpgradeRequestHandler: ChannelInboundHandler, RemovableChannelHandler {
Expand Down
4 changes: 2 additions & 2 deletions Sources/WebSocketKit/WebSocket+Connect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension WebSocket {
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
guard let url = URL(string: url) else {
return eventLoopGroup.next().makeFailedFuture(WebSocketClient.Error.invalidURL)
return eventLoopGroup.any().makeFailedFuture(WebSocketClient.Error.invalidURL)
}
return self.connect(
to: url,
Expand Down Expand Up @@ -174,7 +174,7 @@ extension WebSocket {
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
guard let url = URL(string: url) else {
return eventLoopGroup.next().makeFailedFuture(WebSocketClient.Error.invalidURL)
return eventLoopGroup.any().makeFailedFuture(WebSocketClient.Error.invalidURL)
}
let scheme = url.scheme ?? "ws"
return self.connect(
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebSocketKit/WebSocket.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NIO
import NIOCore
import NIOWebSocket
import NIOHTTP1
import NIOSSL
Expand Down
10 changes: 4 additions & 6 deletions Sources/WebSocketKit/WebSocketClient.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import NIO
import NIOCore
import NIOPosix
import NIOConcurrencyHelpers
import NIOExtras
import NIOHTTP1
Expand All @@ -18,10 +19,7 @@ public final class WebSocketClient {
}
}

public enum EventLoopGroupProvider {
case shared(EventLoopGroup)
case createNew
}
public typealias EventLoopGroupProvider = NIOEventLoopGroupProvider

public struct Configuration {
public var tlsConfiguration: TLSConfiguration?
Expand Down Expand Up @@ -106,7 +104,7 @@ public final class WebSocketClient {
onUpgrade: @escaping (WebSocket) -> ()
) -> EventLoopFuture<Void> {
assert(["ws", "wss"].contains(scheme))
let upgradePromise = self.group.next().makePromise(of: Void.self)
let upgradePromise = self.group.any().makePromise(of: Void.self)
let bootstrap = WebSocketClient.makeBootstrap(on: self.group)
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
.channelInitializer { channel -> EventLoopFuture<Void> in
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebSocketKit/WebSocketHandler.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NIO
import NIOCore
import NIOWebSocket

extension WebSocket {
Expand Down
94 changes: 87 additions & 7 deletions Tests/WebSocketKitTests/AsyncWebSocketKitTests.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import XCTest
import NIO
import NIOHTTP1
import NIOWebSocket
@testable import WebSocketKit

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
final class AsyncWebSocketKitTests: XCTestCase {
func testWebSocketEcho() async throws {
let server = try ServerBootstrap.webSocket(on: self.elg) { req, ws in
let server = try await ServerBootstrap.webSocket(on: self.elg) { req, ws in
ws.onText { ws, text in
ws.send(text)
}
}.bind(host: "localhost", port: 0).wait()
}.bind(host: "localhost", port: 0).get()

guard let port = server.localAddress?.port else {
XCTFail("couldn't get port from \(server.localAddress.debugDescription)")
return
}

let promise = elg.next().makePromise(of: String.self)
let promise = elg.any().makePromise(of: String.self)

try await WebSocket.connect(to: "ws://localhost:\(port)", on: elg) { ws in
do {
Expand All @@ -42,7 +40,91 @@ final class AsyncWebSocketKitTests: XCTestCase {
try await server.close(mode: .all)
}

func testAlternateWebsocketConnectMethods() async throws {
let server = try await ServerBootstrap.webSocket(on: self.elg) { $1.onText { $0.send($1) } }.bind(host: "localhost", port: 0).get()
let promise = self.elg.any().makePromise(of: Void.self)
guard let port = server.localAddress?.port else {
return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
}
try await WebSocket.connect(scheme: "ws", host: "localhost", port: port, on: self.elg) { (ws) async in
do { try await ws.send("hello") } catch { promise.fail(error); try? await ws.close() }
ws.onText { ws, _ in
promise.succeed(())
do { try await ws.close() } catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
}
}
try await promise.futureResult.get()
try await server.close(mode: .all)
}

func testBadURLInWebsocketConnect() async throws {
do {
try await WebSocket.connect(to: "%w", on: self.elg, onUpgrade: { _ async in })
XCTAssertThrowsError({}())
} catch {
XCTAssertThrowsError(try { throw error }()) {
guard case .invalidURL = $0 as? WebSocketClient.Error else {
return XCTFail("Expected .invalidURL but got \(String(reflecting: $0))")
}
}
}
}

func testOnBinary() async throws {
let server = try await ServerBootstrap.webSocket(on: self.elg) { $1.onBinary { $0.send($1) } }.bind(host: "localhost", port: 0).get()
let promise = self.elg.any().makePromise(of: [UInt8].self)
guard let port = server.localAddress?.port else {
return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
}
try await WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { ws in
do { try await ws.send([0x01]) } catch { promise.fail(error); try? await ws.close() }
ws.onBinary { ws, buf in
promise.succeed(.init(buf.readableBytesView))
do { try await ws.close() }
catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
}
}
let result = try await promise.futureResult.get()
XCTAssertEqual(result, [0x01])
try await server.close(mode: .all)
}

func testSendPing() async throws {
let server = try await ServerBootstrap.webSocket(on: self.elg) { _, _ in }.bind(host: "localhost", port: 0).get()
let promise = self.elg.any().makePromise(of: Void.self)
guard let port = server.localAddress?.port else {
return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
}
try await WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { (ws) async in
do { try await ws.sendPing() } catch { promise.fail(error); try? await ws.close() }
ws.onPong {
promise.succeed(())
do { try await $0.close() } catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
}
}
try await promise.futureResult.get()
try await server.close(mode: .all)
}

func testSetPingInterval() async throws {
let server = try await ServerBootstrap.webSocket(on: self.elg) { _, _ in }.bind(host: "localhost", port: 0).get()
let promise = self.elg.any().makePromise(of: Void.self)
guard let port = server.localAddress?.port else {
return XCTFail("couldn't get port from \(String(reflecting: server.localAddress))")
}
try await WebSocket.connect(to: "ws://localhost:\(port)", on: self.elg) { (ws) async in
ws.pingInterval = .milliseconds(100)
ws.onPong {
promise.succeed(())
do { try await $0.close() } catch { XCTFail("Failed to close websocket: \(String(reflecting: error))") }
}
}
try await promise.futureResult.get()
try await server.close(mode: .all)
}

var elg: EventLoopGroup!

override func setUp() {
// needs to be at least two to avoid client / server on same EL timing issues
self.elg = MultiThreadedEventLoopGroup(numberOfThreads: 2)
Expand All @@ -51,5 +133,3 @@ final class AsyncWebSocketKitTests: XCTestCase {
try! self.elg.syncShutdownGracefully()
}
}

#endif
Loading

0 comments on commit 17c0afb

Please sign in to comment.