Skip to content

Commit c2dcf4a

Browse files
committed
Initial Commit
0 parents  commit c2dcf4a

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Package.resolved

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"originHash" : "76f1d97cf3f7ad3cc525dd1359c86dfa0fcdf71d503489656f1fb03a622c3cc2",
3+
"pins" : [
4+
{
5+
"identity" : "swift-atomics",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/apple/swift-atomics.git",
8+
"state" : {
9+
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
10+
"version" : "1.2.0"
11+
}
12+
},
13+
{
14+
"identity" : "swift-collections",
15+
"kind" : "remoteSourceControl",
16+
"location" : "https://github.com/apple/swift-collections.git",
17+
"state" : {
18+
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
19+
"version" : "1.1.0"
20+
}
21+
},
22+
{
23+
"identity" : "swift-log",
24+
"kind" : "remoteSourceControl",
25+
"location" : "https://github.com/apple/swift-log.git",
26+
"state" : {
27+
"revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5",
28+
"version" : "1.5.4"
29+
}
30+
},
31+
{
32+
"identity" : "swift-nio",
33+
"kind" : "remoteSourceControl",
34+
"location" : "https://github.com/apple/swift-nio.git",
35+
"state" : {
36+
"revision" : "359c461e5561d22c6334828806cc25d759ca7aa6",
37+
"version" : "2.65.0"
38+
}
39+
},
40+
{
41+
"identity" : "swift-nio-ssl",
42+
"kind" : "remoteSourceControl",
43+
"location" : "https://github.com/apple/swift-nio-ssl.git",
44+
"state" : {
45+
"revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5",
46+
"version" : "2.26.0"
47+
}
48+
},
49+
{
50+
"identity" : "swift-system",
51+
"kind" : "remoteSourceControl",
52+
"location" : "https://github.com/apple/swift-system.git",
53+
"state" : {
54+
"revision" : "f9266c85189c2751589a50ea5aec72799797e471",
55+
"version" : "1.3.0"
56+
}
57+
}
58+
],
59+
"version" : 3
60+
}

Package.swift

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version: 5.10
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "protoo-swift",
8+
platforms: [.macOS(.v14)],
9+
products: [
10+
.library(name: "protoo-client", targets: ["protoo-client"]),
11+
],
12+
dependencies: [
13+
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
14+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.60.0"),
15+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.26.0"),
16+
],
17+
targets: [
18+
.target(name: "protoo-client", dependencies: [
19+
.product(name: "Logging", package: "swift-log"),
20+
.product(name: "NIOCore", package: "swift-nio"),
21+
.product(name: "NIOFoundationCompat", package: "swift-nio"),
22+
.product(name: "NIOWebSocket", package: "swift-nio"),
23+
.product(name: "NIOSSL", package: "swift-nio-ssl"),
24+
]),
25+
.testTarget(name: "protoo-swiftTests", dependencies: ["protoo-client"]),
26+
]
27+
)

Sources/protoo-client/Message.swift

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Måns Severin on 2024-05-19.
6+
//
7+
8+
import Foundation
9+
10+
public struct Message {
11+
12+
}

Sources/protoo-client/Peer.swift

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
import Foundation
5+
6+
public struct Peer {
7+
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Måns Severin on 2024-05-19.
6+
//
7+
8+
import Foundation
9+
10+
import NIOCore
11+
import NIOPosix
12+
import NIOHTTP1
13+
import NIOSSL
14+
import NIOWebSocket
15+
16+
public final actor WebSocketTransport {
17+
public enum WebSocketTransportError: Error {
18+
case invalidURI(String)
19+
}
20+
21+
public enum WebSocketEvent {
22+
case close
23+
case disconnected
24+
case failed
25+
case message
26+
case open
27+
}
28+
29+
public typealias WebSocketEventHandler = (WebSocketEvent, WebSocketTransport) -> Void
30+
31+
private static let DEFAULT_RETRY_OPTIONS = [
32+
"retries" : 10,
33+
"factor" : 2,
34+
"minTimeout" : 1 * 1000,
35+
"maxTimeout" : 8 * 1000
36+
]
37+
38+
private enum State {
39+
case idle
40+
case connecting(Task<Void, Error>)
41+
case connected(Channel)
42+
}
43+
44+
private let uri: URL
45+
private let group: MultiThreadedEventLoopGroup
46+
47+
private var state: State = .idle
48+
private var eventHandlers: [WebSocketEvent: [WebSocketEventHandler]] = [:]
49+
50+
public init(uri: URL, group: MultiThreadedEventLoopGroup) {
51+
self.uri = uri
52+
self.group = group
53+
}
54+
55+
public func connect() {
56+
guard case .idle = state else {
57+
return
58+
}
59+
60+
state = .connecting(.init {
61+
62+
})
63+
}
64+
65+
public func disconnect() {
66+
67+
}
68+
69+
public func on(_ event: WebSocketEvent, handler: @escaping WebSocketEventHandler) -> Self {
70+
return self
71+
}
72+
73+
private func connect(retriesLeft: Int = 10) async throws {
74+
guard let components = URLComponents(url: uri, resolvingAgainstBaseURL: true) else {
75+
throw WebSocketTransportError.invalidURI("invalid uri")
76+
}
77+
78+
guard let host = components.host else {
79+
throw WebSocketTransportError.invalidURI("invalid uri: no host")
80+
}
81+
82+
guard let scheme = components.scheme else {
83+
throw WebSocketTransportError.invalidURI("invalid uri: no scheme")
84+
}
85+
86+
guard scheme == "ws" || scheme == "wss" else {
87+
throw WebSocketTransportError.invalidURI("invalid uri: scheme is not ws or wss")
88+
}
89+
90+
let port = components.port ?? (scheme == "wss" ? 443 : 80)
91+
let path = components.path
92+
let bootstrap = WebSocketBootstrap(scheme: scheme, host: host, port: port, path: path, group: group)
93+
let channel = try await bootstrap.connect()
94+
}
95+
96+
// /// This method handles the upgrade result.
97+
// private func handleUpgradeResult(_ upgradeResult: EventLoopFuture<UpgradeResult>) async throws {
98+
// switch try await upgradeResult.get() {
99+
// case .websocket(let websocketChannel):
100+
// print("Handling websocket connection")
101+
// try await self.handleWebsocketChannel(websocketChannel)
102+
// print("Done handling websocket connection")
103+
// case .failed:
104+
// // The upgrade to websocket did not succeed. We are just exiting in this case.
105+
// print("Upgrade declined")
106+
// }
107+
// }
108+
//
109+
// private func handleWebsocketChannel(_ channel: NIOAsyncChannel<WebSocketFrame, WebSocketFrame>) async throws {
110+
// // We are sending a ping frame and then
111+
// // start to handle all inbound frames.
112+
//
113+
// let pingFrame = WebSocketFrame(fin: true, opcode: .ping, data: ByteBuffer(string: "Hello!"))
114+
// try await channel.executeThenClose { inbound, outbound in
115+
// try await outbound.write(pingFrame)
116+
//
117+
// for try await frame in inbound {
118+
// switch frame.opcode {
119+
// case .pong:
120+
// print("Received pong: \(String(buffer: frame.data))")
121+
//
122+
// case .text:
123+
// print("Received: \(String(buffer: frame.data))")
124+
//
125+
// case .connectionClose:
126+
// // Handle a received close frame. We're just going to close by returning from this method.
127+
// print("Received Close instruction from server")
128+
// return
129+
// case .binary, .continuation, .ping:
130+
// // We ignore these frames.
131+
// break
132+
// default:
133+
// // Unknown frames are errors.
134+
// return
135+
// }
136+
// }
137+
// }
138+
// }
139+
}
140+
141+
struct WebSocketBootstrap {
142+
enum WebSocketBootstrapError: Error {
143+
case upgradeFailed
144+
}
145+
146+
let scheme: String
147+
let host: String
148+
let port: Int
149+
let path: String
150+
let group: EventLoopGroup
151+
152+
private enum UpgradeResult {
153+
case websocket(NIOAsyncChannel<WebSocketFrame, WebSocketFrame>)
154+
case failed
155+
}
156+
157+
func connect() async throws -> NIOAsyncChannel<WebSocketFrame, WebSocketFrame> {
158+
let initializer: @Sendable (Channel) -> EventLoopFuture<EventLoopFuture<UpgradeResult>> = { channel in
159+
channel.eventLoop.makeCompletedFuture {
160+
let upgrader = NIOTypedWebSocketClientUpgrader<UpgradeResult>(upgradePipelineHandler: { (channel, _) in
161+
channel.eventLoop.makeCompletedFuture {
162+
return UpgradeResult.websocket(try NIOAsyncChannel<WebSocketFrame, WebSocketFrame>(wrappingChannelSynchronously: channel))
163+
}
164+
})
165+
166+
var headers = HTTPHeaders()
167+
168+
headers.add(name: "Host", value: host)
169+
headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
170+
headers.add(name: "Content-Length", value: "0")
171+
headers.add(name: "Sec-WebSocket-Protocol", value: "protoo")
172+
173+
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: path, headers: headers)
174+
175+
let clientUpgradeConfiguration = NIOTypedHTTPClientUpgradeConfiguration(
176+
upgradeRequestHead: requestHead,
177+
upgraders: [upgrader],
178+
notUpgradingCompletionHandler: { channel in
179+
channel.eventLoop.makeCompletedFuture {
180+
return UpgradeResult.failed
181+
}
182+
}
183+
)
184+
185+
let negotiationResultFuture = try channel.pipeline.syncOperations.configureUpgradableHTTPClientPipeline(
186+
configuration: .init(upgradeConfiguration: clientUpgradeConfiguration)
187+
)
188+
189+
return negotiationResultFuture
190+
}
191+
}
192+
193+
let upgradeResult: EventLoopFuture<UpgradeResult>
194+
195+
if scheme == "wss" {
196+
let configuration = TLSConfiguration.makeClientConfiguration()
197+
let sslContext = try NIOSSLContext(configuration: configuration)
198+
let bootstrap = try NIOClientTCPBootstrap(ClientBootstrap(group: group), tls: NIOSSLClientTLSProvider(context: sslContext, serverHostname: host))
199+
200+
upgradeResult = try await bootstrap
201+
.enableTLS()
202+
.connect(host: host, port: port, channelInitializer: initializer)
203+
} else {
204+
let bootstrap = ClientBootstrap(group: group)
205+
206+
upgradeResult = try await bootstrap.connect(host: host, port: port, channelInitializer: initializer)
207+
}
208+
209+
guard case let .websocket(channel) = try await upgradeResult.get() else {
210+
throw WebSocketBootstrapError.upgradeFailed
211+
}
212+
213+
let aggregator = NIOWebSocketFrameAggregator(minNonFinalFragmentSize: 256,
214+
maxAccumulatedFrameCount: 100,
215+
maxAccumulatedFrameSize: 10 * 1024 * 1024)
216+
217+
try await channel.channel.pipeline.addHandler(aggregator).get()
218+
219+
return channel
220+
}
221+
}
222+
223+
extension NIOClientTCPBootstrap {
224+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
225+
public func connect<Output: Sendable>(
226+
host: String,
227+
port: Int,
228+
channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
229+
) async throws -> Output {
230+
return try await (self.underlyingBootstrap as! ClientBootstrap).connect(host: host, port: port, channelInitializer: channelInitializer)
231+
}
232+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import XCTest
2+
import NIOCore
3+
import NIOPosix
4+
5+
@testable import protoo_client
6+
7+
final class protoo_swift_tests: XCTestCase {
8+
func testExample() async throws {
9+
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
10+
let ws = WebSocketTransport(uri: URL(string: "ws://meet.graphiclife.com:40150/session/pommes")!, group: group)
11+
12+
try await ws.run()
13+
}
14+
}

0 commit comments

Comments
 (0)