123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- //////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // FoundationTransport.swift
- // Starscream
- //
- // Created by Dalton Cherry on 1/23/19.
- // Copyright © 2019 Vluxe. All rights reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- //
- //////////////////////////////////////////////////////////////////////////////////////////////////
- import Foundation
- public enum FoundationTransportError: Error {
- case invalidRequest
- case invalidOutputStream
- case timeout
- }
- public class FoundationTransport: NSObject, Transport, StreamDelegate {
- private weak var delegate: TransportEventClient?
- private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: [])
- private var inputStream: InputStream?
- private var outputStream: OutputStream?
- private var isOpen = false
- private var onConnect: ((InputStream, OutputStream) -> Void)?
- private var isTLS = false
- private var certPinner: CertificatePinning?
-
- public var usingTLS: Bool {
- return self.isTLS
- }
-
- public init(streamConfiguration: ((InputStream, OutputStream) -> Void)? = nil) {
- super.init()
- onConnect = streamConfiguration
- }
-
- deinit {
- inputStream?.delegate = nil
- outputStream?.delegate = nil
- }
-
- public func connect(url: URL, timeout: Double = 10, certificatePinning: CertificatePinning? = nil) {
- guard let parts = url.getParts() else {
- delegate?.connectionChanged(state: .failed(FoundationTransportError.invalidRequest))
- return
- }
- self.certPinner = certificatePinning
- self.isTLS = parts.isTLS
- var readStream: Unmanaged<CFReadStream>?
- var writeStream: Unmanaged<CFWriteStream>?
- let h = parts.host as NSString
- CFStreamCreatePairWithSocketToHost(nil, h, UInt32(parts.port), &readStream, &writeStream)
- inputStream = readStream!.takeRetainedValue()
- outputStream = writeStream!.takeRetainedValue()
- guard let inStream = inputStream, let outStream = outputStream else {
- return
- }
- inStream.delegate = self
- outStream.delegate = self
-
- if isTLS {
- let key = CFStreamPropertyKey(rawValue: kCFStreamPropertySocketSecurityLevel)
- CFReadStreamSetProperty(inStream, key, kCFStreamSocketSecurityLevelNegotiatedSSL)
- CFWriteStreamSetProperty(outStream, key, kCFStreamSocketSecurityLevelNegotiatedSSL)
- }
-
- onConnect?(inStream, outStream)
-
- isOpen = false
- CFReadStreamSetDispatchQueue(inStream, workQueue)
- CFWriteStreamSetDispatchQueue(outStream, workQueue)
- inStream.open()
- outStream.open()
-
-
- workQueue.asyncAfter(deadline: .now() + timeout, execute: { [weak self] in
- guard let s = self else { return }
- if !s.isOpen {
- s.delegate?.connectionChanged(state: .failed(FoundationTransportError.timeout))
- }
- })
- }
-
- public func disconnect() {
- if let stream = inputStream {
- stream.delegate = nil
- CFReadStreamSetDispatchQueue(stream, nil)
- stream.close()
- }
- if let stream = outputStream {
- stream.delegate = nil
- CFWriteStreamSetDispatchQueue(stream, nil)
- stream.close()
- }
- isOpen = false
- outputStream = nil
- inputStream = nil
- }
-
- public func register(delegate: TransportEventClient) {
- self.delegate = delegate
- }
-
- public func write(data: Data, completion: @escaping ((Error?) -> ())) {
- guard let outStream = outputStream else {
- completion(FoundationTransportError.invalidOutputStream)
- return
- }
- var total = 0
- let buffer = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self)
- //NOTE: this might need to be dispatched to the work queue instead of being written inline. TBD.
- while total < data.count {
- let written = outStream.write(buffer, maxLength: data.count)
- if written < 0 {
- completion(FoundationTransportError.invalidOutputStream)
- return
- }
- total += written
- }
- completion(nil)
- }
-
- private func getSecurityData() -> (SecTrust?, String?) {
- #if os(watchOS)
- return (nil, nil)
- #else
- guard let outputStream = outputStream else {
- return (nil, nil)
- }
- let trust = outputStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust?
- var domain = outputStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as! String?
-
- if domain == nil,
- let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? {
- var peerNameLen: Int = 0
- SSLGetPeerDomainNameLength(sslContextOut, &peerNameLen)
- var peerName = Data(count: peerNameLen)
- let _ = peerName.withUnsafeMutableBytes { (peerNamePtr: UnsafeMutablePointer<Int8>) in
- SSLGetPeerDomainName(sslContextOut, peerNamePtr, &peerNameLen)
- }
- if let peerDomain = String(bytes: peerName, encoding: .utf8), peerDomain.count > 0 {
- domain = peerDomain
- }
- }
- return (trust, domain)
- #endif
- }
-
- private func read() {
- guard let stream = inputStream else {
- return
- }
- let maxBuffer = 4096
- let buf = NSMutableData(capacity: maxBuffer)
- let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self)
- let length = stream.read(buffer, maxLength: maxBuffer)
- if length < 1 {
- return
- }
- let data = Data(bytes: buffer, count: length)
- delegate?.connectionChanged(state: .receive(data))
- }
-
- // MARK: - StreamDelegate
-
- open func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
- switch eventCode {
- case .hasBytesAvailable:
- if aStream == inputStream {
- read()
- }
- case .errorOccurred:
- delegate?.connectionChanged(state: .failed(aStream.streamError))
- case .endEncountered:
- if aStream == inputStream {
- delegate?.connectionChanged(state: .cancelled)
- }
- case .openCompleted:
- if aStream == inputStream {
- let (trust, domain) = getSecurityData()
- if let pinner = certPinner, let trust = trust {
- pinner.evaluateTrust(trust: trust, domain: domain, completion: { [weak self] (state) in
- switch state {
- case .success:
- self?.isOpen = true
- self?.delegate?.connectionChanged(state: .connected)
- case .failed(let error):
- self?.delegate?.connectionChanged(state: .failed(error))
- }
-
- })
- } else {
- isOpen = true
- delegate?.connectionChanged(state: .connected)
- }
- }
- case .endEncountered:
- if aStream == inputStream {
- delegate?.connectionChanged(state: .cancelled)
- }
- default:
- break
- }
- }
- }
|