Skip to content

Commit

Permalink
laid the ground work for supporting both the native API and backwards…
Browse files Browse the repository at this point in the history
… compatibility with old versions
  • Loading branch information
daltoniam committed Jun 15, 2019
1 parent b386d1e commit c946f5c
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 212 deletions.
19 changes: 0 additions & 19 deletions Sources/Compression/WSCompression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,22 +241,3 @@ class Compressor {
teardownDeflate()
}
}

//internal extension Data {
// struct UnsafeBytesError: Swift.Error {}
//
// func withUnsafeBytes<ResultType, ContentType>(_ body: (UnsafePointer<ContentType>) throws -> ResultType) rethrows -> ResultType {
// #if swift(>=5.0)
// return try withUnsafeBytes { (innerBody: UnsafeRawBufferPointer) -> ResultType in
// if let baseAddress = innerBody.baseAddress, innerBody.count > 0 {
// let pointer = baseAddress.assumingMemoryBound(to: ContentType.self)
// return try body(pointer)
// } else {
// throw UnsafeBytesError()
// }
// }
// #else
// return try withUnsafeBytes(body)
// #endif
// }
//}
22 changes: 22 additions & 0 deletions Sources/Engine/Engine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Engine.swift
// Starscream
//
// Created by Dalton Cherry on 6/15/19.
// Copyright © 2019 Vluxe. All rights reserved.
//

import Foundation

public protocol EngineDelegate: class {
func didReceive(event: WebSocketEvent)
}

public protocol Engine {
func register(delegate: EngineDelegate)
func start(request: URLRequest)
func stop(closeCode: UInt16)
func forceStop()
func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?)
func write(string: String, completion: (() -> ())?)
}
90 changes: 90 additions & 0 deletions Sources/Engine/NativeEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// NativeEngine.swift
// Starscream
//
// Created by Dalton Cherry on 6/15/19.
// Copyright © 2019 Vluxe. All rights reserved.
//

import Foundation

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public class NativeEngine: Engine {
private var task: URLSessionWebSocketTask?
weak var delegate: EngineDelegate?

public init() {
//TODO: I probably need to drop down into the NWConnection APIs to get this to work with all of Starscream's features
//
//NOTE: URLSessionWebSocketTask doesn't work with our ruby web server in the SimpleTest example.
//It allows crashes. It works fine with https://echo.websocket.org in either http or https. Not sure why though
//needs more debugging and probably needs radar filed.
}

public func register(delegate: EngineDelegate) {
self.delegate = delegate
}

public func start(request: URLRequest) {
task = URLSession.shared.webSocketTask(with: request)
doRead()
task?.resume()
}

public func stop(closeCode: UInt16) {
let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: Int(closeCode)) ?? .normalClosure
task?.cancel(with: closeCode, reason: nil)
}

public func forceStop() {
stop(closeCode: UInt16(URLSessionWebSocketTask.CloseCode.abnormalClosure.rawValue))
}

public func write(string: String, completion: (() -> ())?) {
task?.send(.string(string), completionHandler: { (error) in
completion?()
})
}

public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) {
switch opcode {
case .binaryFrame:
task?.send(.data(data), completionHandler: { (error) in
completion?()
})
case .textFrame:
let text = String(data: data, encoding: .utf8)!
write(string: text, completion: completion)
case .ping:
task?.sendPing(pongReceiveHandler: { (error) in
completion?()
})
default:
break //unsupported
}
}

private func doRead() {
task?.receive { [weak self] (result) in
switch result {
case .success(let message):
switch message {
case .string(let string):
self?.broadcast(event: .text(string))
case .data(let data):
self?.broadcast(event: .binary(data))
@unknown default:
break
}
break
case .failure(let error):
self?.broadcast(event: .error(error))
}
self?.doRead()
}
}

private func broadcast(event: WebSocketEvent) {
delegate?.didReceive(event: event)
}
}
228 changes: 228 additions & 0 deletions Sources/Engine/WSEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
//
// WSEngine.swift
// Starscream
//
// Created by Dalton Cherry on 6/15/19.
// Copyright © 2019 Vluxe. All rights reserved.
//

import Foundation

public class WSEngine: Engine, TransportEventClient, FramerEventClient,
FrameCollectorDelegate, HTTPHandlerDelegate {
private let transport: Transport
private let framer: Framer
private let httpHandler: HTTPHandler
private let compressionHandler: CompressionHandler?
private let certPinner: CertificatePinning?
private let headerChecker: HeaderValidator
private var request: URLRequest!

private let frameHandler = FrameCollector()
private var didUpgrade = false
private var secKeyValue = ""
private let writeQueue = DispatchQueue(label: "com.vluxe.starscream.writequeue")
private let mutex = DispatchSemaphore(value: 1)
private var canSend = false

weak var delegate: EngineDelegate?
public var respondToPingWithPong: Bool = true

public init(transport: Transport,
certPinner: CertificatePinning? = nil,
headerValidator: HeaderValidator = FoundationSecurity(),
httpHandler: HTTPHandler = FoundationHTTPHandler(),
framer: Framer = WSFramer(),
compressionHandler: CompressionHandler? = nil) {
self.transport = transport
self.framer = framer
self.httpHandler = httpHandler
self.certPinner = certPinner
self.headerChecker = headerValidator
self.compressionHandler = compressionHandler
framer.updateCompression(supports: compressionHandler != nil)
frameHandler.delegate = self
}

public func register(delegate: EngineDelegate) {
self.delegate = delegate
}

public func start(request: URLRequest) {
mutex.wait()
let isConnected = canSend
mutex.signal()
if isConnected {
return
}

self.request = request
transport.register(delegate: self)
framer.register(delegate: self)
httpHandler.register(delegate: self)
frameHandler.delegate = self
guard let url = request.url else {
return
}
transport.connect(url: url, timeout: request.timeoutInterval, certificatePinning: certPinner)
}

public func stop(closeCode: UInt16 = CloseCode.normal.rawValue) {
let capacity = MemoryLayout<UInt16>.size
var pointer = [UInt8](repeating: 0, count: capacity)
writeUint16(&pointer, offset: 0, value: closeCode)
let payload = Data(bytes: pointer, count: MemoryLayout<UInt16>.size)
write(data: payload, opcode: .connectionClose, completion: { [weak self] in
self?.reset()
self?.forceStop()
})
}

public func forceStop() {
transport.disconnect()
}

public func write(string: String, completion: (() -> ())?) {
let data = string.data(using: .utf8)!
write(data: data, opcode: .textFrame, completion: completion)
}

public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) {
writeQueue.async { [weak self] in
guard let s = self else { return }
s.mutex.wait()
let canWrite = s.canSend
s.mutex.signal()
if !canWrite {
return
}

var isCompressed = false
var sendData = data
if let compressedData = s.compressionHandler?.compress(data: data) {
sendData = compressedData
isCompressed = true
}

let frameData = s.framer.createWriteFrame(opcode: opcode, payload: sendData, isCompressed: isCompressed)
s.transport.write(data: frameData, completion: {_ in
completion?()
})
}
}

// MARK: - TransportEventClient

public func connectionChanged(state: ConnectionState) {
switch state {
case .connected:
secKeyValue = HTTPWSHeader.generateWebSocketKey()
let wsReq = HTTPWSHeader.createUpgrade(request: request, supportsCompression: framer.supportsCompression(), secKeyValue: secKeyValue)
let data = httpHandler.convert(request: wsReq)
transport.write(data: data, completion: {_ in })
case .waiting:
break
case .failed(let error):
handleError(error)
case .viability(let isViable):
broadcast(event: .viablityChanged(isViable))
case .shouldReconnect(let status):
broadcast(event: .reconnectSuggested(status))
case .receive(let data):
if didUpgrade {
framer.add(data: data)
} else {
let offset = httpHandler.parse(data: data)
if offset > 0 {
let extraData = data.subdata(in: offset..<data.endIndex)
framer.add(data: extraData)
}
}
case .cancelled:
broadcast(event: .cancelled)
}
}

// MARK: - HTTPHandlerDelegate

public func didReceiveHTTP(event: HTTPEvent) {
switch event {
case .success(let headers):
if let error = headerChecker.validate(headers: headers, key: secKeyValue) {
handleError(error)
return
}
mutex.wait()
didUpgrade = true
canSend = true
mutex.signal()
compressionHandler?.load(headers: headers)
broadcast(event: .connected(headers))
case .failure(let error):
handleError(error)
}
}

// MARK: - FramerEventClient

public func frameProcessed(event: FrameEvent) {
switch event {
case .frame(let frame):
frameHandler.add(frame: frame)
case .error(let error):
handleError(error)
}
}

// MARK: - FrameCollectorDelegate

public func decompress(data: Data, isFinal: Bool) -> Data? {
return compressionHandler?.decompress(data: data, isFinal: isFinal)
}

public func didForm(event: FrameCollector.Event) {
switch event {
case .text(let string):
broadcast(event: .text(string))
case .binary(let data):
broadcast(event: .binary(data))
case .pong(let data):
broadcast(event: .pong(data))
case .ping(let data):
broadcast(event: .ping(data))
if respondToPingWithPong {
write(data: data ?? Data(), opcode: .pong, completion: nil)
}
case .closed(let reason, let code):
broadcast(event: .disconnected(reason, code))
stop(closeCode: code)
case .error(let error):
handleError(error)
}
}

private func broadcast(event: WebSocketEvent) {
delegate?.didReceive(event: event)
}

//This call can be coming from a lot of different queues/threads.
//be aware of that when modifying shared variables
private func handleError(_ error: Error?) {
if let wsError = error as? WSError {
stop(closeCode: wsError.code)
} else {
stop()
}

delegate?.didReceive(event: .error(error))
}

private func reset() {
mutex.wait()
canSend = false
didUpgrade = false
mutex.signal()
}


}
2 changes: 1 addition & 1 deletion Sources/Security/FoundationSecurity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension FoundationSecurity: CertificatePinning {
}

private func handleSecurityTrust(trust: SecTrust, completion: ((PinningState) -> ())) {
if #available(iOSApplicationExtension 12.0, *) {
if #available(iOS 12.0, OSX 10.14, *) {
var error: CFError?
if SecTrustEvaluateWithError(trust, &error) {
completion(.success)
Expand Down

0 comments on commit c946f5c

Please sign in to comment.