Skip to content

Commit dcf4ea3

Browse files
committed
SwiftWS
1 parent 3609131 commit dcf4ea3

File tree

9 files changed

+533
-0
lines changed

9 files changed

+533
-0
lines changed

SwiftWS/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1250"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "SwiftWS"
18+
BuildableName = "SwiftWS"
19+
BlueprintName = "SwiftWS"
20+
ReferencedContainer = "container:">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
<BuildActionEntry
24+
buildForTesting = "YES"
25+
buildForRunning = "YES"
26+
buildForProfiling = "NO"
27+
buildForArchiving = "NO"
28+
buildForAnalyzing = "YES">
29+
<BuildableReference
30+
BuildableIdentifier = "primary"
31+
BlueprintIdentifier = "SwiftWSTests"
32+
BuildableName = "SwiftWSTests"
33+
BlueprintName = "SwiftWSTests"
34+
ReferencedContainer = "container:">
35+
</BuildableReference>
36+
</BuildActionEntry>
37+
</BuildActionEntries>
38+
</BuildAction>
39+
<TestAction
40+
buildConfiguration = "Debug"
41+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
42+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
43+
shouldUseLaunchSchemeArgsEnv = "YES">
44+
<Testables>
45+
<TestableReference
46+
skipped = "NO">
47+
<BuildableReference
48+
BuildableIdentifier = "primary"
49+
BlueprintIdentifier = "SwiftWSTests"
50+
BuildableName = "SwiftWSTests"
51+
BlueprintName = "SwiftWSTests"
52+
ReferencedContainer = "container:">
53+
</BuildableReference>
54+
</TestableReference>
55+
</Testables>
56+
</TestAction>
57+
<LaunchAction
58+
buildConfiguration = "Debug"
59+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
60+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
61+
launchStyle = "0"
62+
useCustomWorkingDirectory = "NO"
63+
ignoresPersistentStateOnLaunch = "NO"
64+
debugDocumentVersioning = "YES"
65+
debugServiceExtension = "internal"
66+
allowLocationSimulation = "YES">
67+
</LaunchAction>
68+
<ProfileAction
69+
buildConfiguration = "Release"
70+
shouldUseLaunchSchemeArgsEnv = "YES"
71+
savedToolIdentifier = ""
72+
useCustomWorkingDirectory = "NO"
73+
debugDocumentVersioning = "YES">
74+
<MacroExpansion>
75+
<BuildableReference
76+
BuildableIdentifier = "primary"
77+
BlueprintIdentifier = "SwiftWS"
78+
BuildableName = "SwiftWS"
79+
BlueprintName = "SwiftWS"
80+
ReferencedContainer = "container:">
81+
</BuildableReference>
82+
</MacroExpansion>
83+
</ProfileAction>
84+
<AnalyzeAction
85+
buildConfiguration = "Debug">
86+
</AnalyzeAction>
87+
<ArchiveAction
88+
buildConfiguration = "Release"
89+
revealArchiveInOrganizer = "YES">
90+
</ArchiveAction>
91+
</Scheme>

SwiftWS/Package.resolved

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

SwiftWS/Package.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// swift-tools-version:5.3
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: "SwiftWS",
8+
platforms: [
9+
.iOS(.v12),
10+
.macOS(.v10_14)
11+
],
12+
products: [
13+
// Products define the executables and libraries a package produces, and make them visible to other packages.
14+
.library(
15+
name: "SwiftWS",
16+
targets: ["SwiftWS"]),
17+
],
18+
dependencies: [
19+
// Dependencies declare other packages that this package depends on.
20+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.19.0"),
21+
.package(url: "https://github.com/apple/swift-nio-transport-services", from: "1.10.0")
22+
],
23+
targets: [
24+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
25+
// Targets can depend on other targets in this package, and on products in packages this package depends on.
26+
.target(
27+
name: "SwiftWS",
28+
dependencies: [
29+
.product(name: "NIO", package: "swift-nio"),
30+
.product(name: "NIOHTTP1", package: "swift-nio"),
31+
.product(name: "NIOWebSocket", package: "swift-nio"),
32+
.product(name: "NIOTransportServices", package: "swift-nio-transport-services")
33+
]),
34+
.testTarget(
35+
name: "SwiftWSTests",
36+
dependencies: ["SwiftWS"]),
37+
]
38+
)

SwiftWS/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SwiftWS
2+
3+
A description of this package.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
16+
import NIO
17+
import NIOHTTP1
18+
import NIOWebSocket
19+
import NIOTransportServices
20+
import Network
21+
import Foundation
22+
23+
final class WebSocketTimeHandler: ChannelInboundHandler {
24+
25+
private var ws: SwiftWS.WebSocket
26+
27+
init(_ ws: SwiftWS.WebSocket){
28+
self.ws = ws
29+
}
30+
31+
typealias InboundIn = WebSocketFrame
32+
typealias OutboundOut = WebSocketFrame
33+
34+
private var awaitingClose: Bool = false
35+
36+
public func handlerAdded(context: ChannelHandlerContext) {
37+
self.checkForMessage(context: context)
38+
}
39+
40+
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
41+
let frame = self.unwrapInboundIn(data)
42+
43+
switch frame.opcode {
44+
case .connectionClose:
45+
ws.onCloseAction?(.init(wasClean: true, code: 0, reason: "Close event received", target: ws))
46+
self.receivedClose(context: context, frame: frame)
47+
case .ping:
48+
self.pong(context: context, frame: frame)
49+
case .text:
50+
var data = frame.unmaskedData
51+
let text = data.readString(length: data.readableBytes) ?? ""
52+
ws.onMessageAction?(.init(data: text, type: "", target: ws))
53+
case .binary, .continuation, .pong:
54+
// We ignore these frames.
55+
break
56+
default:
57+
// Unknown frames are errors.
58+
ws.onCloseAction?(.init(wasClean: false, code: 0, reason: "Unknown frames errors", target: ws))
59+
self.closeOnError(context: context)
60+
}
61+
}
62+
63+
public func channelReadComplete(context: ChannelHandlerContext) {
64+
context.flush()
65+
}
66+
67+
private func checkForMessage(context: ChannelHandlerContext) {
68+
guard context.channel.isActive else { return }
69+
70+
// We can't send if we sent a close message.
71+
guard !self.awaitingClose else { return }
72+
73+
if ws.isClosing {
74+
var buffer = context.channel.allocator.buffer(capacity: 2)
75+
buffer.writeInteger(UInt16(ws.closeCode))
76+
buffer.writeString(ws.closeReason)
77+
let closeFrame = WebSocketFrame(fin: true, opcode: .connectionClose, data: buffer)
78+
_ = context.writeAndFlush(self.wrapOutboundOut(closeFrame))
79+
awaitingClose = true
80+
return
81+
}
82+
// We can't really check for error here, but it's also not the purpose of the
83+
// example so let's not worry about it.
84+
if !ws.messageStack.isEmpty{
85+
for message in ws.messageStack {
86+
let buffer = context.channel.allocator.buffer(string: message)
87+
let frame = WebSocketFrame(fin: true, opcode: .text, data: buffer)
88+
context.write(self.wrapOutboundOut(frame))
89+
.whenFailure { (_: Error) in
90+
context.close(promise: nil)
91+
}
92+
}
93+
ws.messageStack.removeAll()
94+
context.flush()
95+
}
96+
97+
context.eventLoop.scheduleTask(in: .milliseconds(100), { self.checkForMessage(context: context) })
98+
}
99+
100+
private func receivedClose(context: ChannelHandlerContext, frame: WebSocketFrame) {
101+
// Handle a received close frame. In websockets, we're just going to send the close
102+
// frame and then close, unless we already sent our own close frame.
103+
if awaitingClose {
104+
// Cool, we started the close and were waiting for the user. We're done.
105+
context.close(promise: nil)
106+
} else {
107+
// This is an unsolicited close. We're going to send a response frame and
108+
// then, when we've sent it, close up shop. We should send back the close code the remote
109+
// peer sent us, unless they didn't send one at all.
110+
var data = frame.unmaskedData
111+
let closeDataCode = data.readSlice(length: 2) ?? ByteBuffer()
112+
let closeFrame = WebSocketFrame(fin: true, opcode: .connectionClose, data: closeDataCode)
113+
_ = context.write(self.wrapOutboundOut(closeFrame)).map { () in
114+
context.close(promise: nil)
115+
}
116+
}
117+
}
118+
119+
private func pong(context: ChannelHandlerContext, frame: WebSocketFrame) {
120+
var frameData = frame.data
121+
let maskingKey = frame.maskKey
122+
123+
if let maskingKey = maskingKey {
124+
frameData.webSocketUnmask(maskingKey)
125+
}
126+
127+
let responseFrame = WebSocketFrame(fin: true, opcode: .pong, data: frameData)
128+
context.write(self.wrapOutboundOut(responseFrame), promise: nil)
129+
}
130+
131+
private func closeOnError(context: ChannelHandlerContext) {
132+
// We have hit an error, we want to close. We do that by sending a close frame and then
133+
// shutting down the write side of the connection.
134+
var data = context.channel.allocator.buffer(capacity: 2)
135+
data.write(webSocketErrorCode: .protocolError)
136+
let frame = WebSocketFrame(fin: true, opcode: .connectionClose, data: data)
137+
context.write(self.wrapOutboundOut(frame)).whenComplete { (_: Result<Void, Error>) in
138+
context.close(mode: .output, promise: nil)
139+
}
140+
awaitingClose = true
141+
}
142+
}

0 commit comments

Comments
 (0)