From 4fac45de2e36a7dbbbd43b6b65f00412189a4efa Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 14:52:58 -0700 Subject: [PATCH 01/14] WIP: Moving Starscream Websocket into ApolloWebSocket --- Apollo.xcodeproj/project.pbxproj | 20 +- Sources/ApolloWebSocket/Compression.swift | 173 +++ .../ApolloWebSocket/DefaultWebSocket.swift | 70 - .../SSLClientCertificate.swift | 94 ++ Sources/ApolloWebSocket/SSLSecurity.swift | 255 ++++ Sources/ApolloWebSocket/WebSocket.swift | 1272 +++++++++++++++++ Sources/ApolloWebSocket/WebSocketClient.swift | 3 + .../ApolloWebSocket/WebSocketTransport.swift | 1 + 8 files changed, 1814 insertions(+), 74 deletions(-) create mode 100644 Sources/ApolloWebSocket/Compression.swift delete mode 100644 Sources/ApolloWebSocket/DefaultWebSocket.swift create mode 100644 Sources/ApolloWebSocket/SSLClientCertificate.swift create mode 100644 Sources/ApolloWebSocket/SSLSecurity.swift create mode 100644 Sources/ApolloWebSocket/WebSocket.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 7518d56e45..c2d1c57011 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -190,6 +190,10 @@ DE0586362669957800265760 /* CacheReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE664ED92666DF150054DB4F /* CacheReference.swift */; }; DE0586372669958F00265760 /* GraphQLError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */; }; DE0586392669985000265760 /* Dictionary+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0586382669985000265760 /* Dictionary+Helpers.swift */; }; + DE181A2C26C5C0CB000C0B9C /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */; }; + DE181A2E26C5C299000C0B9C /* SSLClientCertificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */; }; + DE181A3026C5C38E000C0B9C /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */; }; + DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; @@ -199,7 +203,6 @@ DE6B156A261505660068D642 /* GraphQLMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B154A261505450068D642 /* GraphQLMap.swift */; }; DE6B15AF26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B15AE26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift */; }; DE6B15B126152BE10068D642 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; - DE8C84F5268BC42100C54D02 /* DefaultWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */; }; DECD46D0262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */; }; DECD46FB262F659500924527 /* ApolloCodegenLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B7B6F47233C26D100F32205 /* ApolloCodegenLib.framework */; }; DECD4736262F668500924527 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; @@ -744,6 +747,10 @@ DE05862426697A8C00265760 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DE0586322669948500265760 /* InputValue+Evaluation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputValue+Evaluation.swift"; sourceTree = ""; }; DE0586382669985000265760 /* Dictionary+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Helpers.swift"; sourceTree = ""; }; + DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = ""; }; + DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLClientCertificate.swift; sourceTree = ""; }; + DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLSecurity.swift; sourceTree = ""; }; + DE181A3126C5C401000C0B9C /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compression.swift; sourceTree = ""; }; DE3C7973260A646300D2F4FF /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSet.swift; sourceTree = ""; }; DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDict.swift; sourceTree = ""; }; @@ -779,7 +786,6 @@ DE6B160B26152D210068D642 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; DE6B160C26152D210068D642 /* Workspace-Packaging.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Workspace-Packaging.xcconfig"; sourceTree = ""; }; DE6B160D26152D210068D642 /* Workspace-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Workspace-Shared.xcconfig"; sourceTree = ""; }; - DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWebSocket.swift; sourceTree = ""; }; DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarWarsApolloSchemaDownloaderTests.swift; sourceTree = ""; }; DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApolloCodegenTestSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DECD490D262F81BF00924527 /* ApolloCodegenTestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApolloCodegenTestSupport.h; sourceTree = ""; }; @@ -1168,13 +1174,16 @@ isa = PBXGroup; children = ( 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */, - DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */, + DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */, 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */, 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */, 9B7BDA9423FDE94C00ACD198 /* WebSocketError.swift */, 9B7BDA9523FDE94C00ACD198 /* WebSocketTask.swift */, 9B7BDA9923FDE94C00ACD198 /* WebSocketTransport.swift */, 9B7BDA9A23FDE94C00ACD198 /* Info.plist */, + DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */, + DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */, + DE181A3126C5C401000C0B9C /* Compression.swift */, ); name = ApolloWebSocket; path = Sources/ApolloWebSocket; @@ -2443,12 +2452,15 @@ buildActionMask = 2147483647; files = ( 9B7BDA9F23FDE94C00ACD198 /* WebSocketClient.swift in Sources */, + DE181A2C26C5C0CB000C0B9C /* WebSocket.swift in Sources */, + DE181A2E26C5C299000C0B9C /* SSLClientCertificate.swift in Sources */, 9B7BDAA023FDE94C00ACD198 /* WebSocketTransport.swift in Sources */, 9B7BDA9C23FDE94C00ACD198 /* WebSocketTask.swift in Sources */, - DE8C84F5268BC42100C54D02 /* DefaultWebSocket.swift in Sources */, + DE181A3026C5C38E000C0B9C /* SSLSecurity.swift in Sources */, 9B7BDA9B23FDE94C00ACD198 /* WebSocketError.swift in Sources */, 9B7BDA9D23FDE94C00ACD198 /* SplitNetworkTransport.swift in Sources */, 9B7BDA9E23FDE94C00ACD198 /* OperationMessage.swift in Sources */, + DE181A3226C5C401000C0B9C /* Compression.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift new file mode 100644 index 0000000000..a9f993f080 --- /dev/null +++ b/Sources/ApolloWebSocket/Compression.swift @@ -0,0 +1,173 @@ +// Created by Joseph Ross on 7/16/14. +// Copyright © 2017 Joseph Ross. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) + +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + +// Compression implementation is implemented in conformance with RFC 7692 Compression Extensions +// for WebSocket: https://tools.ietf.org/html/rfc7692 + +import Foundation +import zlib + +enum CompressionError: Swift.Error { + case dataBufferEmpty +} + +class Decompressor { + private var strm = z_stream() + private var buffer = [UInt8](repeating: 0, count: 0x2000) + private var inflateInitialized = false + private let windowBits:Int + + init?(windowBits:Int) { + self.windowBits = windowBits + guard initInflate() else { return nil } + } + + private func initInflate() -> Bool { + if Z_OK == inflateInit2_(&strm, -CInt(windowBits), + ZLIB_VERSION, CInt(MemoryLayout.size)) + { + inflateInitialized = true + return true + } + return false + } + + func reset() throws { + teardownInflate() + guard initInflate() else { throw WSError(type: .compressionError, message: "Error for decompressor on reset", code: 0) } + } + + func decompress(_ data: Data, finish: Bool) throws -> Data { + return try data.withUnsafeBytes { pointer -> Data in + guard let bytes = pointer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + throw CompressionError.dataBufferEmpty + } + return try decompress(bytes: bytes, count: data.count, finish: finish) + } + } + + func decompress(bytes: UnsafePointer, count: Int, finish: Bool) throws -> Data { + var decompressed = Data() + try decompress(bytes: bytes, count: count, out: &decompressed) + + if finish { + let tail:[UInt8] = [0x00, 0x00, 0xFF, 0xFF] + try decompress(bytes: tail, count: tail.count, out: &decompressed) + } + + return decompressed + + } + + private func decompress(bytes: UnsafePointer, count: Int, out: inout Data) throws { + var res:CInt = 0 + strm.next_in = UnsafeMutablePointer(mutating: bytes) + strm.avail_in = CUnsignedInt(count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = inflate(&strm, 0) + + let byteCount = buffer.count - Int(strm.avail_out) + out.append(buffer, count: byteCount) + } while res == Z_OK && strm.avail_out == 0 + + guard (res == Z_OK && strm.avail_out > 0) + || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) + else { + throw WSError(type: .compressionError, message: "Error on decompressing", code: 0) + } + } + + private func teardownInflate() { + if inflateInitialized, Z_OK == inflateEnd(&strm) { + inflateInitialized = false + } + } + + deinit { + teardownInflate() + } +} + +class Compressor { + private var strm = z_stream() + private var buffer = [UInt8](repeating: 0, count: 0x2000) + private var deflateInitialized = false + private let windowBits:Int + + init?(windowBits: Int) { + self.windowBits = windowBits + guard initDeflate() else { return nil } + } + + private func initDeflate() -> Bool { + if Z_OK == deflateInit2_(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -CInt(windowBits), 8, Z_DEFAULT_STRATEGY, + ZLIB_VERSION, CInt(MemoryLayout.size)) + { + deflateInitialized = true + return true + } + return false + } + + func reset() throws { + teardownDeflate() + guard initDeflate() else { throw WSError(type: .compressionError, message: "Error for compressor on reset", code: 0) } + } + + func compress(_ data: Data) throws -> Data { + var compressed = Data() + var res:CInt = 0 + try data.withUnsafeBytes { pointer -> Void in + guard let bytes = pointer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + throw CompressionError.dataBufferEmpty + } + + strm.next_in = UnsafeMutablePointer(mutating: bytes) + strm.avail_in = CUnsignedInt(data.count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = deflate(&strm, Z_SYNC_FLUSH) + + let byteCount = buffer.count - Int(strm.avail_out) + compressed.append(buffer, count: byteCount) + } + while res == Z_OK && strm.avail_out == 0 + + } + + guard res == Z_OK && strm.avail_out > 0 + || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) + else { + throw WSError(type: .compressionError, message: "Error on compressing", code: 0) + } + + compressed.removeLast(4) + return compressed + } + + private func teardownDeflate() { + if deflateInitialized, Z_OK == deflateEnd(&strm) { + deflateInitialized = false + } + } + + deinit { + teardownDeflate() + } +} + diff --git a/Sources/ApolloWebSocket/DefaultWebSocket.swift b/Sources/ApolloWebSocket/DefaultWebSocket.swift deleted file mode 100644 index 3465209936..0000000000 --- a/Sources/ApolloWebSocket/DefaultWebSocket.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Starscream -import Foundation - -/// Included default implementation of a `WebSocketClient`, based on `Starscream`'s `WebSocket`. -public class DefaultWebSocket: WebSocketClient, Starscream.WebSocketDelegate { - - /// The websocket protocols supported by this websocket client implementation. - static private let wsProtocols = ["graphql-ws"] - - /// The underlying `Starscream` websocket used by this websocket client. - private let underlyingWebsocket: Starscream.WebSocket - - public var request: URLRequest { - get { underlyingWebsocket.request } - set { underlyingWebsocket.request = newValue } - } - - public weak var delegate: WebSocketClientDelegate? - - public var callbackQueue: DispatchQueue { - get { underlyingWebsocket.callbackQueue } - set { underlyingWebsocket.callbackQueue = newValue } - } - - /// Required initializer - /// - /// - Parameters: - /// - request: The URLRequest to use on connection. - /// - certPinner: [optional] The object providing information about certificate pinning. Should default to Starscream's `FoundationSecurity`. - /// - compressionHandler: [optional] The object helping with any compression handling. Should default to nil. - required public init(request: URLRequest) { - self.underlyingWebsocket = Starscream.WebSocket( - request: request, - protocols: Self.wsProtocols, - stream: FoundationStream()) - self.underlyingWebsocket.delegate = self - } - - public func connect() { - self.underlyingWebsocket.connect() - } - - public func disconnect() { - self.underlyingWebsocket.disconnect() - } - - public func write(ping: Data, completion: (() -> Void)?) { - self.underlyingWebsocket.write(ping: ping, completion: completion) - } - - public func write(string: String) { - self.underlyingWebsocket.write(string: string) - } - - public func websocketDidConnect(socket: Starscream.WebSocketClient) { - self.delegate?.websocketDidConnect(socket: self) - } - - public func websocketDidDisconnect(socket: Starscream.WebSocketClient, error: Error?) { - self.delegate?.websocketDidDisconnect(socket: self, error: error) - } - - public func websocketDidReceiveMessage(socket: Starscream.WebSocketClient, text: String) { - self.delegate?.websocketDidReceiveMessage(socket: self, text: text) - } - - public func websocketDidReceiveData(socket: Starscream.WebSocketClient, data: Data) { - self.delegate?.websocketDidReceiveData(socket: self, data: data) - } -} diff --git a/Sources/ApolloWebSocket/SSLClientCertificate.swift b/Sources/ApolloWebSocket/SSLClientCertificate.swift new file mode 100644 index 0000000000..21a6681ae0 --- /dev/null +++ b/Sources/ApolloWebSocket/SSLClientCertificate.swift @@ -0,0 +1,94 @@ +// Created by Tomasz Trela on 08/03/2018. +// Copyright © 2018 Vluxe. All rights reserved. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) + +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + +import Foundation + +public struct SSLClientCertificateError: LocalizedError { + public var errorDescription: String? + + init(errorDescription: String) { + self.errorDescription = errorDescription + } +} + +public class SSLClientCertificate { + internal let streamSSLCertificates: NSArray + + /** + Convenience init. + - parameter pkcs12Path: Path to pkcs12 file containing private key and X.509 ceritifacte (.p12) + - parameter password: file password, see **kSecImportExportPassphrase** + */ + public convenience init(pkcs12Path: String, password: String) throws { + let pkcs12Url = URL(fileURLWithPath: pkcs12Path) + do { + try self.init(pkcs12Url: pkcs12Url, password: password) + } catch { + throw error + } + } + + /** + Designated init. For more information, see SSLSetCertificate() in Security/SecureTransport.h. + - parameter identity: SecIdentityRef, see **kCFStreamSSLCertificates** + - parameter identityCertificate: CFArray of SecCertificateRefs, see **kCFStreamSSLCertificates** + */ + public init(identity: SecIdentity, identityCertificate: SecCertificate) { + self.streamSSLCertificates = NSArray(objects: identity, identityCertificate) + } + + /** + Convenience init. + - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) + - parameter password: file password, see **kSecImportExportPassphrase** + */ + public convenience init(pkcs12Url: URL, password: String) throws { + let importOptions = [kSecImportExportPassphrase as String : password] as CFDictionary + do { + try self.init(pkcs12Url: pkcs12Url, importOptions: importOptions) + } catch { + throw error + } + } + + /** + Designated init. + - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) + - parameter importOptions: A dictionary containing import options. A + kSecImportExportPassphrase entry is required at minimum. Only password-based + PKCS12 blobs are currently supported. See **SecImportExport.h** + */ + public init(pkcs12Url: URL, importOptions: CFDictionary) throws { + do { + let pkcs12Data = try Data(contentsOf: pkcs12Url) + var rawIdentitiesAndCertificates: CFArray? + let pkcs12CFData: CFData = pkcs12Data as CFData + let importStatus = SecPKCS12Import(pkcs12CFData, importOptions, &rawIdentitiesAndCertificates) + + guard importStatus == errSecSuccess else { + throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecPKCS12Import', see 'SecBase.h' - OSStatus: \(importStatus)") + } + guard let identitiyAndCertificate = (rawIdentitiesAndCertificates as? Array>)?.first else { + throw SSLClientCertificateError(errorDescription: "(Starscream) Error - PKCS12 file is empty") + } + + let identity = identitiyAndCertificate[kSecImportItemIdentity as String] as! SecIdentity + var identityCertificate: SecCertificate? + let copyStatus = SecIdentityCopyCertificate(identity, &identityCertificate) + guard copyStatus == errSecSuccess else { + throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecIdentityCopyCertificate', see 'SecBase.h' - OSStatus: \(copyStatus)") + } + self.streamSSLCertificates = NSArray(objects: identity, identityCertificate!) + } catch { + throw error + } + } +} + diff --git a/Sources/ApolloWebSocket/SSLSecurity.swift b/Sources/ApolloWebSocket/SSLSecurity.swift new file mode 100644 index 0000000000..90aa8ee96a --- /dev/null +++ b/Sources/ApolloWebSocket/SSLSecurity.swift @@ -0,0 +1,255 @@ +// Created by Dalton Cherry on 5/16/15. +// Copyright (c) 2014-2016 Dalton Cherry. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) + +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + +#if os(Linux) +#else +import Foundation +import Security + +public protocol SSLTrustValidator { + func isValid(_ trust: SecTrust, domain: String?) -> Bool +} + +open class SSLCert { + var certData: Data? + var key: SecKey? + + /** + Designated init for certificates + + - parameter data: is the binary data of the certificate + + - returns: a representation security object to be used with + */ + public init(data: Data) { + self.certData = data + } + + /** + Designated init for public keys + + - parameter key: is the public key to be used + + - returns: a representation security object to be used with + */ + public init(key: SecKey) { + self.key = key + } +} + +open class SSLSecurity : SSLTrustValidator { + public var validatedDN = true //should the domain name be validated? + public var validateEntireChain = true //should the entire cert chain be validated + + var isReady = false //is the key processing done? + var certificates: [Data]? //the certificates + var pubKeys: [SecKey]? //the public keys + var usePublicKeys = false //use public keys or certificate validation? + + /** + Use certs from main app bundle + + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public convenience init(usePublicKeys: Bool = false) { + let paths = Bundle.main.paths(forResourcesOfType: "cer", inDirectory: ".") + + let certs = paths.reduce([SSLCert]()) { (certs: [SSLCert], path: String) -> [SSLCert] in + var certs = certs + if let data = NSData(contentsOfFile: path) { + certs.append(SSLCert(data: data as Data)) + } + return certs + } + + self.init(certs: certs, usePublicKeys: usePublicKeys) + } + + /** + Designated init + + - parameter certs: is the certificates or public keys to use + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public init(certs: [SSLCert], usePublicKeys: Bool) { + self.usePublicKeys = usePublicKeys + + if self.usePublicKeys { + DispatchQueue.global(qos: .default).async { + let pubKeys = certs.reduce([SecKey]()) { (pubKeys: [SecKey], cert: SSLCert) -> [SecKey] in + var pubKeys = pubKeys + if let data = cert.certData, cert.key == nil { + cert.key = self.extractPublicKey(data) + } + if let key = cert.key { + pubKeys.append(key) + } + return pubKeys + } + + self.pubKeys = pubKeys + self.isReady = true + } + } else { + let certificates = certs.reduce([Data]()) { (certificates: [Data], cert: SSLCert) -> [Data] in + var certificates = certificates + if let data = cert.certData { + certificates.append(data) + } + return certificates + } + self.certificates = certificates + self.isReady = true + } + } + + /** + Valid the trust and domain name. + + - parameter trust: is the serverTrust to validate + - parameter domain: is the CN domain to validate + + - returns: if the key was successfully validated + */ + open func isValid(_ trust: SecTrust, domain: String?) -> Bool { + + var tries = 0 + while !self.isReady { + usleep(1000) + tries += 1 + if tries > 5 { + return false //doesn't appear it is going to ever be ready... + } + } + var policy: SecPolicy + if self.validatedDN { + policy = SecPolicyCreateSSL(true, domain as NSString?) + } else { + policy = SecPolicyCreateBasicX509() + } + SecTrustSetPolicies(trust,policy) + if self.usePublicKeys { + if let keys = self.pubKeys { + let serverPubKeys = publicKeyChain(trust) + for serverKey in serverPubKeys as [AnyObject] { + for key in keys as [AnyObject] { + if serverKey.isEqual(key) { + return true + } + } + } + } + } else if let certs = self.certificates { + let serverCerts = certificateChain(trust) + var collect = [SecCertificate]() + for cert in certs { + collect.append(SecCertificateCreateWithData(nil,cert as CFData)!) + } + SecTrustSetAnchorCertificates(trust,collect as NSArray) + var result: SecTrustResultType = .unspecified + SecTrustEvaluate(trust,&result) + if result == .unspecified || result == .proceed { + if !validateEntireChain { + return true + } + var trustedCount = 0 + for serverCert in serverCerts { + for cert in certs { + if cert == serverCert { + trustedCount += 1 + break + } + } + } + if trustedCount == serverCerts.count { + return true + } + } + } + return false + } + + /** + Get the public key from a certificate data + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + public func extractPublicKey(_ data: Data) -> SecKey? { + guard let cert = SecCertificateCreateWithData(nil, data as CFData) else { return nil } + + return extractPublicKey(cert, policy: SecPolicyCreateBasicX509()) + } + + /** + Get the public key from a certificate + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + public func extractPublicKey(_ cert: SecCertificate, policy: SecPolicy) -> SecKey? { + var possibleTrust: SecTrust? + SecTrustCreateWithCertificates(cert, policy, &possibleTrust) + + guard let trust = possibleTrust else { return nil } + var result: SecTrustResultType = .unspecified + SecTrustEvaluate(trust, &result) + return SecTrustCopyPublicKey(trust) + } + + /** + Get the certificate chain for the trust + + - parameter trust: is the trust to lookup the certificate chain for + + - returns: the certificate chain for the trust + */ + public func certificateChain(_ trust: SecTrust) -> [Data] { + let certificates = (0.. [Data] in + var certificates = certificates + let cert = SecTrustGetCertificateAtIndex(trust, index) + certificates.append(SecCertificateCopyData(cert!) as Data) + return certificates + } + + return certificates + } + + /** + Get the public key chain for the trust + + - parameter trust: is the trust to lookup the certificate chain and extract the public keys + + - returns: the public keys from the certifcate chain for the trust + */ + public func publicKeyChain(_ trust: SecTrust) -> [SecKey] { + let policy = SecPolicyCreateBasicX509() + let keys = (0.. [SecKey] in + var keys = keys + let cert = SecTrustGetCertificateAtIndex(trust, index) + if let key = extractPublicKey(cert!, policy: policy) { + keys.append(key) + } + + return keys + } + + return keys + } + + +} +#endif diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift new file mode 100644 index 0000000000..763886b0fd --- /dev/null +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -0,0 +1,1272 @@ +// Created by Dalton Cherry on 7/16/14. +// Copyright (c) 2014-2017 Dalton Cherry. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) + +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + + +import Foundation +import CoreFoundation +import CommonCrypto + +let WebsocketDidConnectNotification = "WebsocketDidConnectNotification" +let WebsocketDidDisconnectNotification = "WebsocketDidDisconnectNotification" +let WebsocketDisconnectionErrorKeyName = "WebsocketDisconnectionErrorKeyName" + +//Standard WebSocket close codes +public enum CloseCode : UInt16 { + case normal = 1000 + case goingAway = 1001 + case protocolError = 1002 + case protocolUnhandledType = 1003 + // 1004 reserved. + case noStatusReceived = 1005 + //1006 reserved. + case encoding = 1007 + case policyViolated = 1008 + case messageTooBig = 1009 +} + +public enum ErrorType: Error { + case outputStreamWriteError //output stream error during write + case compressionError + case invalidSSLError //Invalid SSL certificate + case writeTimeoutError //The socket timed out waiting to be ready to write + case protocolError //There was an error parsing the WebSocket frames + case upgradeError //There was an error during the HTTP upgrade + case closeError //There was an error during the close (socket probably has been dereferenced) +} + +public struct WSError: Error { + public let type: ErrorType + public let message: String + public let code: Int +} + +//SSL settings for the stream +public struct SSLSettings { + public let useSSL: Bool + public let disableCertValidation: Bool + public var overrideTrustHostname: Bool + public var desiredTrustHostname: String? + public let sslClientCertificate: SSLClientCertificate? + #if os(Linux) + #else + public let cipherSuites: [SSLCipherSuite]? + #endif +} + +public protocol WSStreamDelegate: class { + func newBytesInStream() + func streamDidError(error: Error?) +} + +//This protocol is to allow custom implemention of the underlining stream. This way custom socket libraries (e.g. linux) can be used +public protocol WSStream { + var delegate: WSStreamDelegate? {get set} + func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping ((Error?) -> Void)) + func write(data: Data) -> Int + func read() -> Data? + func cleanup() + #if os(Linux) || os(watchOS) + #else + func sslTrust() -> (trust: SecTrust?, domain: String?) + #endif +} + +open class FoundationStream : NSObject, WSStream, StreamDelegate { + private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) + private var inputStream: InputStream? + private var outputStream: OutputStream? + public weak var delegate: WSStreamDelegate? + let BUFFER_MAX = 4096 + + public var enableSOCKSProxy = false + + public func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping ((Error?) -> Void)) { + var readStream: Unmanaged? + var writeStream: Unmanaged? + let h = url.host! as NSString + CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream) + inputStream = readStream!.takeRetainedValue() + outputStream = writeStream!.takeRetainedValue() + + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + if enableSOCKSProxy { + let proxyDict = CFNetworkCopySystemProxySettings() + let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue()) + let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy) + CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig) + CFReadStreamSetProperty(inputStream, propertyKey, socksConfig) + } + #endif + + guard let inStream = inputStream, let outStream = outputStream else { return } + inStream.delegate = self + outStream.delegate = self + if ssl.useSSL { + inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + var settings = [NSObject: NSObject]() + if ssl.disableCertValidation { + settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: false) + } + if ssl.overrideTrustHostname { + if let hostname = ssl.desiredTrustHostname { + settings[kCFStreamSSLPeerName] = hostname as NSString + } else { + settings[kCFStreamSSLPeerName] = kCFNull + } + } + if let sslClientCertificate = ssl.sslClientCertificate { + settings[kCFStreamSSLCertificates] = sslClientCertificate.streamSSLCertificates + } + + inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + #endif + + #if os(Linux) + #else + if let cipherSuites = ssl.cipherSuites { + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?, + let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? { + let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) + let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) + if resIn != errSecSuccess { + completion(WSError(type: .invalidSSLError, message: "Error setting ingoing cypher suites", code: Int(resIn))) + } + if resOut != errSecSuccess { + completion(WSError(type: .invalidSSLError, message: "Error setting outgoing cypher suites", code: Int(resOut))) + } + } + #endif + } + #endif + } + + CFReadStreamSetDispatchQueue(inStream, workQueue) + CFWriteStreamSetDispatchQueue(outStream, workQueue) + inStream.open() + outStream.open() + + var out = timeout// wait X seconds before giving up + workQueue.async { [weak self] in + while !outStream.hasSpaceAvailable { + usleep(100) // wait until the socket is ready + out -= 100 + if out < 0 { + completion(WSError(type: .writeTimeoutError, message: "Timed out waiting for the socket to be ready for a write", code: 0)) + return + } else if let error = outStream.streamError { + completion(error) + return // disconnectStream will be called. + } else if self == nil { + completion(WSError(type: .closeError, message: "socket object has been dereferenced", code: 0)) + return + } + } + completion(nil) //success! + } + } + + public func write(data: Data) -> Int { + guard let outStream = outputStream else {return -1} + let buffer = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self) + return outStream.write(buffer, maxLength: data.count) + } + + public func read() -> Data? { + guard let stream = inputStream else {return nil} + let buf = NSMutableData(capacity: BUFFER_MAX) + let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) + let length = stream.read(buffer, maxLength: BUFFER_MAX) + if length < 1 { + return nil + } + return Data(bytes: buffer, count: length) + } + + public func cleanup() { + if let stream = inputStream { + stream.delegate = nil + CFReadStreamSetDispatchQueue(stream, nil) + stream.close() + } + if let stream = outputStream { + stream.delegate = nil + CFWriteStreamSetDispatchQueue(stream, nil) + stream.close() + } + outputStream = nil + inputStream = nil + } + + #if os(Linux) || os(watchOS) + #else + public func sslTrust() -> (trust: SecTrust?, domain: String?) { + 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) in + SSLGetPeerDomainName(sslContextOut, peerNamePtr, &peerNameLen) + } + if let peerDomain = String(bytes: peerName, encoding: .utf8), peerDomain.count > 0 { + domain = peerDomain + } + } + + return (trust, domain) + } + #endif + + /** + Delegate for the stream methods. Processes incoming bytes + */ + open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + if eventCode == .hasBytesAvailable { + if aStream == inputStream { + delegate?.newBytesInStream() + } + } else if eventCode == .errorOccurred { + delegate?.streamDidError(error: aStream.streamError) + } else if eventCode == .endEncountered { + delegate?.streamDidError(error: nil) + } + } +} + +//WebSocket implementation + +open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegate { + + public enum OpCode : UInt8 { + case continueFrame = 0x0 + case textFrame = 0x1 + case binaryFrame = 0x2 + // 3-7 are reserved. + case connectionClose = 0x8 + case ping = 0x9 + case pong = 0xA + // B-F reserved. + } + + public static let ErrorDomain = "WebSocket" + + // Where the callback is executed. It defaults to the main UI thread queue. + public var callbackQueue = DispatchQueue.main + + // MARK: - Constants + + let headerWSUpgradeName = "Upgrade" + let headerWSUpgradeValue = "websocket" + let headerWSHostName = "Host" + let headerWSConnectionName = "Connection" + let headerWSConnectionValue = "Upgrade" + let headerWSProtocolName = "Sec-WebSocket-Protocol" + let headerWSVersionName = "Sec-WebSocket-Version" + let headerWSVersionValue = "13" + let headerWSExtensionName = "Sec-WebSocket-Extensions" + let headerWSKeyName = "Sec-WebSocket-Key" + let headerOriginName = "Origin" + let headerWSAcceptName = "Sec-WebSocket-Accept" + let BUFFER_MAX = 4096 + let FinMask: UInt8 = 0x80 + let OpCodeMask: UInt8 = 0x0F + let RSVMask: UInt8 = 0x70 + let RSV1Mask: UInt8 = 0x40 + let MaskMask: UInt8 = 0x80 + let PayloadLenMask: UInt8 = 0x7F + let MaxFrameSize: Int = 32 + let httpSwitchProtocolCode = 101 + let supportedSSLSchemes = ["wss", "https"] + + public class WSResponse { + var isFin = false + public var code: OpCode = .continueFrame + var bytesLeft = 0 + public var frameCount = 0 + public var buffer: NSMutableData? + public let firstFrame = { + return Date() + }() + } + + // MARK: - Delegates + + /// Responds to callback about new messages coming in over the WebSocket + /// and also connection/disconnect messages. + public weak var delegate: WebSocketClientDelegate? + + public var onConnect: (() -> Void)? + public var onDisconnect: ((Error?) -> Void)? + public var onText: ((String) -> Void)? + public var onData: ((Data) -> Void)? + public var onPong: ((Data?) -> Void)? + public var onHttpResponseHeaders: (([String: String]) -> Void)? + + public var disableSSLCertValidation = false + public var overrideTrustHostname = false + public var desiredTrustHostname: String? = nil + public var sslClientCertificate: SSLClientCertificate? = nil + + public var enableCompression = true + #if os(Linux) + #else + public var security: SSLTrustValidator? + public var enabledSSLCipherSuites: [SSLCipherSuite]? + #endif + + public var isConnected: Bool { + mutex.lock() + let isConnected = connected + mutex.unlock() + return isConnected + } + public var request: URLRequest //this is only public to allow headers, timeout, etc to be modified on reconnect + public var currentURL: URL { return request.url! } + + public var respondToPingWithPong: Bool = true + + // MARK: - Private + + private struct CompressionState { + var supportsCompression = false + var messageNeedsDecompression = false + var serverMaxWindowBits = 15 + var clientMaxWindowBits = 15 + var clientNoContextTakeover = false + var serverNoContextTakeover = false + var decompressor:Decompressor? = nil + var compressor:Compressor? = nil + } + + private var stream: WSStream + private var connected = false + private var isConnecting = false + private let mutex = NSLock() + private var compressionState = CompressionState() + private var writeQueue = OperationQueue() + private var readStack = [WSResponse]() + private var inputQueue = [Data]() + private var fragBuffer: Data? + private var certValidated = false + private var didDisconnect = false + private var readyToWrite = false + private var headerSecKey = "" + private var canDispatch: Bool { + mutex.lock() + let canWork = readyToWrite + mutex.unlock() + return canWork + } + + /// Used for setting protocols. + public init(request: URLRequest, protocols: [String]? = nil, stream: WSStream = FoundationStream()) { + self.request = request + self.stream = stream + if request.value(forHTTPHeaderField: headerOriginName) == nil { + guard let url = request.url else {return} + var origin = url.absoluteString + if let hostUrl = URL (string: "/", relativeTo: url) { + origin = hostUrl.absoluteString + origin.remove(at: origin.index(before: origin.endIndex)) + } + self.request.setValue(origin, forHTTPHeaderField: headerOriginName) + } + if let protocols = protocols, !protocols.isEmpty { + self.request.setValue(protocols.joined(separator: ","), forHTTPHeaderField: headerWSProtocolName) + } + writeQueue.maxConcurrentOperationCount = 1 + } + + public convenience init(url: URL, protocols: [String]? = nil) { + var request = URLRequest(url: url) + request.timeoutInterval = 5 + self.init(request: request, protocols: protocols) + } + + // Used for specifically setting the QOS for the write queue. + public convenience init(url: URL, writeQueueQOS: QualityOfService, protocols: [String]? = nil) { + self.init(url: url, protocols: protocols) + writeQueue.qualityOfService = writeQueueQOS + } + + /** + Connect to the WebSocket server on a background thread. + */ + open func connect() { + guard !isConnecting else { return } + didDisconnect = false + isConnecting = true + createHTTPRequest() + } + + /** + Disconnect from the server. I send a Close control frame to the server, then expect the server to respond with a Close control frame and close the socket from its end. I notify my delegate once the socket has been closed. + + If you supply a non-nil `forceTimeout`, I wait at most that long (in seconds) for the server to close the socket. After the timeout expires, I close the socket and notify my delegate. + + If you supply a zero (or negative) `forceTimeout`, I immediately close the socket (without sending a Close control frame) and notify my delegate. + + - Parameter forceTimeout: Maximum time to wait for the server to close the socket. + - Parameter closeCode: The code to send on disconnect. The default is the normal close code for cleanly disconnecting a webSocket. + */ + open func disconnect( + forceTimeout: TimeInterval? = nil, + closeCode: UInt16 = CloseCode.normal.rawValue + ) { + guard isConnected else { return } + switch forceTimeout { + case .some(let seconds) where seconds > 0: + let milliseconds = Int(seconds * 1_000) + callbackQueue.asyncAfter(deadline: .now() + .milliseconds(milliseconds)) { [weak self] in + self?.disconnectStream(nil) + } + fallthrough + case .none: + writeError(closeCode) + default: + disconnectStream(nil) + break + } + } + + public func disconnect() { + self.disconnect(forceTimeout: nil, closeCode: CloseCode.normal.rawValue) + } + + /** + Write a string to the websocket. This sends it as a text frame. + + If you supply a non-nil completion block, I will perform it when the write completes. + + - parameter string: The string to write. + - parameter completion: The (optional) completion handler. + */ + open func write(string: String, completion: (() -> ())? = nil) { + guard isConnected else { return } + dequeueWrite(string.data(using: String.Encoding.utf8)!, code: .textFrame, writeCompletion: completion) + } + + public func write(string: String) { + self.write(string: string, completion: nil) + } + + /** + Write binary data to the websocket. This sends it as a binary frame. + + If you supply a non-nil completion block, I will perform it when the write completes. + + - parameter data: The data to write. + - parameter completion: The (optional) completion handler. + */ + open func write(data: Data, completion: (() -> ())? = nil) { + guard isConnected else { return } + dequeueWrite(data, code: .binaryFrame, writeCompletion: completion) + } + + /** + Write a ping to the websocket. This sends it as a control frame. + */ + open func write(ping: Data, completion: (() -> ())? = nil) { + guard isConnected else { return } + dequeueWrite(ping, code: .ping, writeCompletion: completion) + } + + /** + Write a pong to the websocket. This sends it as a control frame. + */ + open func write(pong: Data, completion: (() -> ())? = nil) { + guard isConnected else { return } + dequeueWrite(pong, code: .pong, writeCompletion: completion) + } + + /** + Private method that starts the connection. + */ + private func createHTTPRequest() { + guard let url = request.url else {return} + var port = url.port + if port == nil { + if supportedSSLSchemes.contains(url.scheme!) { + port = 443 + } else { + port = 80 + } + } + request.setValue(headerWSUpgradeValue, forHTTPHeaderField: headerWSUpgradeName) + request.setValue(headerWSConnectionValue, forHTTPHeaderField: headerWSConnectionName) + headerSecKey = generateWebSocketKey() + request.setValue(headerWSVersionValue, forHTTPHeaderField: headerWSVersionName) + request.setValue(headerSecKey, forHTTPHeaderField: headerWSKeyName) + + if enableCompression { + let val = "permessage-deflate; client_max_window_bits; server_max_window_bits=15" + request.setValue(val, forHTTPHeaderField: headerWSExtensionName) + } + let hostValue = request.allHTTPHeaderFields?[headerWSHostName] ?? "\(url.host!):\(port!)" + request.setValue(hostValue, forHTTPHeaderField: headerWSHostName) + + var path = url.absoluteString + let offset = (url.scheme?.count ?? 2) + 3 + path = String(path[path.index(path.startIndex, offsetBy: offset).. String { + var key = "" + let seed = 16 + for _ in 0.., bufferLen: Int) { + let code = processHTTP(buffer, bufferLen: bufferLen) + switch code { + case 0: + break + case -1: + fragBuffer = Data(bytes: buffer, count: bufferLen) + break // do nothing, we are going to collect more data + default: + doDisconnect(WSError(type: .upgradeError, message: "Invalid HTTP upgrade", code: code)) + } + } + + /** + Finds the HTTP Packet in the TCP stream, by looking for the CRLF. + */ + private func processHTTP(_ buffer: UnsafePointer, bufferLen: Int) -> Int { + let CRLFBytes = [UInt8(ascii: "\r"), UInt8(ascii: "\n"), UInt8(ascii: "\r"), UInt8(ascii: "\n")] + var k = 0 + var totalSize = 0 + for i in 0.. 0 { + let code = validateResponse(buffer, bufferLen: totalSize) + if code != 0 { + return code + } + isConnecting = false + mutex.lock() + connected = true + mutex.unlock() + didDisconnect = false + if canDispatch { + callbackQueue.async { [weak self] in + guard let self = self else { return } + self.onConnect?() + self.delegate?.websocketDidConnect(socket: self) + NotificationCenter.default.post(name: NSNotification.Name(WebsocketDidConnectNotification), object: self) + } + } + //totalSize += 1 //skip the last \n + let restSize = bufferLen - totalSize + if restSize > 0 { + processRawMessagesInBuffer(buffer + totalSize, bufferLen: restSize) + } + return 0 //success + } + return -1 // Was unable to find the full TCP header. + } + + /** + Validates the HTTP is a 101 as per the RFC spec. + */ + private func validateResponse(_ buffer: UnsafePointer, bufferLen: Int) -> Int { + guard let str = String(data: Data(bytes: buffer, count: bufferLen), encoding: .utf8) else { return -1 } + let splitArr = str.components(separatedBy: "\r\n") + var code = -1 + var i = 0 + var headers = [String: String]() + for str in splitArr { + if i == 0 { + let responseSplit = str.components(separatedBy: .whitespaces) + guard responseSplit.count > 1 else { return -1 } + if let c = Int(responseSplit[1]) { + code = c + } + } else { + let responseSplit = str.components(separatedBy: ":") + guard responseSplit.count > 1 else { break } + let key = responseSplit[0].trimmingCharacters(in: .whitespaces) + let val = responseSplit[1].trimmingCharacters(in: .whitespaces) + headers[key.lowercased()] = val + } + i += 1 + } + onHttpResponseHeaders?(headers) + if code != httpSwitchProtocolCode { + return code + } + + if let extensionHeader = headers[headerWSExtensionName.lowercased()] { + processExtensionHeader(extensionHeader) + } + + if let acceptKey = headers[headerWSAcceptName.lowercased()] { + if acceptKey.count > 0 { + if headerSecKey.count > 0 { + let sha = "\(headerSecKey)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1Base64() + if sha != acceptKey as String { + return -1 + } + } + return 0 + } + } + return -1 + } + + /** + Parses the extension header, setting up the compression parameters. + */ + func processExtensionHeader(_ extensionHeader: String) { + let parts = extensionHeader.components(separatedBy: ";") + for p in parts { + let part = p.trimmingCharacters(in: .whitespaces) + if part == "permessage-deflate" { + compressionState.supportsCompression = true + } else if part.hasPrefix("server_max_window_bits=") { + let valString = part.components(separatedBy: "=")[1] + if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { + compressionState.serverMaxWindowBits = val + } + } else if part.hasPrefix("client_max_window_bits=") { + let valString = part.components(separatedBy: "=")[1] + if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { + compressionState.clientMaxWindowBits = val + } + } else if part == "client_no_context_takeover" { + compressionState.clientNoContextTakeover = true + } else if part == "server_no_context_takeover" { + compressionState.serverNoContextTakeover = true + } + } + if compressionState.supportsCompression { + compressionState.decompressor = Decompressor(windowBits: compressionState.serverMaxWindowBits) + compressionState.compressor = Compressor(windowBits: compressionState.clientMaxWindowBits) + } + } + + /** + Read a 16 bit big endian value from a buffer + */ + private static func readUint16(_ buffer: UnsafePointer, offset: Int) -> UInt16 { + return (UInt16(buffer[offset + 0]) << 8) | UInt16(buffer[offset + 1]) + } + + /** + Read a 64 bit big endian value from a buffer + */ + private static func readUint64(_ buffer: UnsafePointer, offset: Int) -> UInt64 { + var value = UInt64(0) + for i in 0...7 { + value = (value << 8) | UInt64(buffer[offset + i]) + } + return value + } + + /** + Write a 16-bit big endian value to a buffer. + */ + private static func writeUint16(_ buffer: UnsafeMutablePointer, offset: Int, value: UInt16) { + buffer[offset + 0] = UInt8(value >> 8) + buffer[offset + 1] = UInt8(value & 0xff) + } + + /** + Write a 64-bit big endian value to a buffer. + */ + private static func writeUint64(_ buffer: UnsafeMutablePointer, offset: Int, value: UInt64) { + for i in 0...7 { + buffer[offset + i] = UInt8((value >> (8*UInt64(7 - i))) & 0xff) + } + } + + /** + Process one message at the start of `buffer`. Return another buffer (sharing storage) that contains the leftover contents of `buffer` that I didn't process. + */ + private func processOneRawMessage(inBuffer buffer: UnsafeBufferPointer) -> UnsafeBufferPointer { + let response = readStack.last + guard let baseAddress = buffer.baseAddress else {return emptyBuffer} + let bufferLen = buffer.count + if response != nil && bufferLen < 2 { + fragBuffer = Data(buffer: buffer) + return emptyBuffer + } + if let response = response, response.bytesLeft > 0 { + var len = response.bytesLeft + var extra = bufferLen - response.bytesLeft + if response.bytesLeft > bufferLen { + len = bufferLen + extra = 0 + } + response.bytesLeft -= len + response.buffer?.append(Data(bytes: baseAddress, count: len)) + _ = processResponse(response) + return buffer.fromOffset(bufferLen - extra) + } else { + let isFin = (FinMask & baseAddress[0]) + let receivedOpcodeRawValue = (OpCodeMask & baseAddress[0]) + let receivedOpcode = OpCode(rawValue: receivedOpcodeRawValue) + let isMasked = (MaskMask & baseAddress[1]) + let payloadLen = (PayloadLenMask & baseAddress[1]) + var offset = 2 + if compressionState.supportsCompression && receivedOpcode != .continueFrame { + compressionState.messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 + } + if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !compressionState.messageNeedsDecompression { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "masked and rsv data is not currently supported", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + let isControlFrame = (receivedOpcode == .connectionClose || receivedOpcode == .ping) + if !isControlFrame && (receivedOpcode != .binaryFrame && receivedOpcode != .continueFrame && + receivedOpcode != .textFrame && receivedOpcode != .pong) { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "unknown opcode: \(receivedOpcodeRawValue)", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + if isControlFrame && isFin == 0 { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "control frames can't be fragmented", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + var closeCode = CloseCode.normal.rawValue + if receivedOpcode == .connectionClose { + if payloadLen == 1 { + closeCode = CloseCode.protocolError.rawValue + } else if payloadLen > 1 { + closeCode = WebSocket.readUint16(baseAddress, offset: offset) + if closeCode < 1000 || (closeCode > 1003 && closeCode < 1007) || (closeCode > 1013 && closeCode < 3000) { + closeCode = CloseCode.protocolError.rawValue + } + } + if payloadLen < 2 { + doDisconnect(WSError(type: .protocolError, message: "connection closed by server", code: Int(closeCode))) + writeError(closeCode) + return emptyBuffer + } + } else if isControlFrame && payloadLen > 125 { + writeError(CloseCode.protocolError.rawValue) + return emptyBuffer + } + var dataLength = UInt64(payloadLen) + if dataLength == 127 { + dataLength = WebSocket.readUint64(baseAddress, offset: offset) + offset += MemoryLayout.size + } else if dataLength == 126 { + dataLength = UInt64(WebSocket.readUint16(baseAddress, offset: offset)) + offset += MemoryLayout.size + } + if bufferLen < offset || UInt64(bufferLen - offset) < dataLength { + fragBuffer = Data(bytes: baseAddress, count: bufferLen) + return emptyBuffer + } + var len = dataLength + if dataLength > UInt64(bufferLen) { + len = UInt64(bufferLen-offset) + } + if receivedOpcode == .connectionClose && len > 0 { + let size = MemoryLayout.size + offset += size + len -= UInt64(size) + } + let data: Data + if compressionState.messageNeedsDecompression, let decompressor = compressionState.decompressor { + do { + data = try decompressor.decompress(bytes: baseAddress+offset, count: Int(len), finish: isFin > 0) + if isFin > 0 && compressionState.serverNoContextTakeover { + try decompressor.reset() + } + } catch { + let closeReason = "Decompression failed: \(error)" + let closeCode = CloseCode.encoding.rawValue + doDisconnect(WSError(type: .protocolError, message: closeReason, code: Int(closeCode))) + writeError(closeCode) + return emptyBuffer + } + } else { + data = Data(bytes: baseAddress+offset, count: Int(len)) + } + + if receivedOpcode == .connectionClose { + var closeReason = "connection closed by server" + if let customCloseReason = String(data: data, encoding: .utf8) { + closeReason = customCloseReason + } else { + closeCode = CloseCode.protocolError.rawValue + } + doDisconnect(WSError(type: .protocolError, message: closeReason, code: Int(closeCode))) + writeError(closeCode) + return emptyBuffer + } + if receivedOpcode == .pong { + if canDispatch { + callbackQueue.async { [weak self] in + guard let self = self else { return } + let pongData: Data? = data.count > 0 ? data : nil + self.onPong?(pongData) + } + } + return buffer.fromOffset(offset + Int(len)) + } + var response = readStack.last + if isControlFrame { + response = nil // Don't append pings. + } + if isFin == 0 && receivedOpcode == .continueFrame && response == nil { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "continue frame before a binary or text frame", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + var isNew = false + if response == nil { + if receivedOpcode == .continueFrame { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "first frame can't be a continue frame", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + isNew = true + response = WSResponse() + response!.code = receivedOpcode! + response!.bytesLeft = Int(dataLength) + response!.buffer = NSMutableData(data: data) + } else { + if receivedOpcode == .continueFrame { + response!.bytesLeft = Int(dataLength) + } else { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(WSError(type: .protocolError, message: "second and beyond of fragment message must be a continue frame", code: Int(errCode))) + writeError(errCode) + return emptyBuffer + } + response!.buffer!.append(data) + } + if let response = response { + response.bytesLeft -= Int(len) + response.frameCount += 1 + response.isFin = isFin > 0 ? true : false + if isNew { + readStack.append(response) + } + _ = processResponse(response) + } + + let step = Int(offset + numericCast(len)) + return buffer.fromOffset(step) + } + } + + /** + Process all messages in the buffer if possible. + */ + private func processRawMessagesInBuffer(_ pointer: UnsafePointer, bufferLen: Int) { + var buffer = UnsafeBufferPointer(start: pointer, count: bufferLen) + repeat { + buffer = processOneRawMessage(inBuffer: buffer) + } while buffer.count >= 2 + if buffer.count > 0 { + fragBuffer = Data(buffer: buffer) + } + } + + /** + Process the finished response of a buffer. + */ + private func processResponse(_ response: WSResponse) -> Bool { + if response.isFin && response.bytesLeft <= 0 { + if response.code == .ping { + if respondToPingWithPong { + let data = response.buffer! // local copy so it is perverse for writing + dequeueWrite(data as Data, code: .pong) + } + } else if response.code == .textFrame { + guard let str = String(data: response.buffer! as Data, encoding: .utf8) else { + writeError(CloseCode.encoding.rawValue) + return false + } + if canDispatch { + callbackQueue.async { [weak self] in + guard let self = self else { return } + self.onText?(str) + self.delegate?.websocketDidReceiveMessage(socket: self, text: str) + } + } + } else if response.code == .binaryFrame { + if canDispatch { + let data = response.buffer! // local copy so it is perverse for writing + callbackQueue.async { [weak self] in + guard let self = self else { return } + self.onData?(data as Data) + self.delegate?.websocketDidReceiveData(socket: self, data: data as Data) + } + } + } + readStack.removeLast() + return true + } + return false + } + + /** + Write an error to the socket + */ + private func writeError(_ code: UInt16) { + let buf = NSMutableData(capacity: MemoryLayout.size) + let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) + WebSocket.writeUint16(buffer, offset: 0, value: code) + dequeueWrite(Data(bytes: buffer, count: MemoryLayout.size), code: .connectionClose) + } + + /** + Used to write things to the stream + */ + private func dequeueWrite(_ data: Data, code: OpCode, writeCompletion: (() -> ())? = nil) { + let operation = BlockOperation() + operation.addExecutionBlock { [weak self, weak operation] in + //stream isn't ready, let's wait + guard let self = self else { return } + guard let sOperation = operation else { return } + var offset = 2 + var firstByte:UInt8 = self.FinMask | code.rawValue + var data = data + if [.textFrame, .binaryFrame].contains(code), let compressor = self.compressionState.compressor { + do { + data = try compressor.compress(data) + if self.compressionState.clientNoContextTakeover { + try compressor.reset() + } + firstByte |= self.RSV1Mask + } catch { + // TODO: report error? We can just send the uncompressed frame. + } + } + let dataLength = data.count + let frame = NSMutableData(capacity: dataLength + self.MaxFrameSize) + let buffer = UnsafeMutableRawPointer(frame!.mutableBytes).assumingMemoryBound(to: UInt8.self) + buffer[0] = firstByte + if dataLength < 126 { + buffer[1] = CUnsignedChar(dataLength) + } else if dataLength <= Int(UInt16.max) { + buffer[1] = 126 + WebSocket.writeUint16(buffer, offset: offset, value: UInt16(dataLength)) + offset += MemoryLayout.size + } else { + buffer[1] = 127 + WebSocket.writeUint64(buffer, offset: offset, value: UInt64(dataLength)) + offset += MemoryLayout.size + } + buffer[1] |= self.MaskMask + let maskKey = UnsafeMutablePointer(buffer + offset) + _ = SecRandomCopyBytes(kSecRandomDefault, Int(MemoryLayout.size), maskKey) + offset += MemoryLayout.size + + for i in 0...size] + offset += 1 + } + var total = 0 + while !sOperation.isCancelled { + if !self.readyToWrite { + self.doDisconnect(WSError(type: .outputStreamWriteError, message: "output stream had an error during write", code: 0)) + break + } + let stream = self.stream + let writeBuffer = UnsafeRawPointer(frame!.bytes+total).assumingMemoryBound(to: UInt8.self) + let len = stream.write(data: Data(bytes: writeBuffer, count: offset-total)) + if len <= 0 { + self.doDisconnect(WSError(type: .outputStreamWriteError, message: "output stream had an error during write", code: 0)) + break + } else { + total += len + } + if total >= offset { + if let callback = writeCompletion { + self.callbackQueue.async { + callback() + } + } + + break + } + } + } + writeQueue.addOperation(operation) + } + + /** + Used to preform the disconnect delegate + */ + private func doDisconnect(_ error: Error?) { + guard !didDisconnect else { return } + didDisconnect = true + isConnecting = false + mutex.lock() + connected = false + mutex.unlock() + guard canDispatch else {return} + callbackQueue.async { [weak self] in + guard let self = self else { return } + self.onDisconnect?(error) + self.delegate?.websocketDidDisconnect(socket: self, error: error) + let userInfo = error.map{ [WebsocketDisconnectionErrorKeyName: $0] } + NotificationCenter.default.post(name: NSNotification.Name(WebsocketDidDisconnectNotification), object: self, userInfo: userInfo) + } + } + + // MARK: - Deinit + + deinit { + mutex.lock() + readyToWrite = false + cleanupStream() + mutex.unlock() + writeQueue.cancelAllOperations() + } + +} + +private extension String { + func sha1Base64() -> String { + let data = self.data(using: String.Encoding.utf8)! + var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) + data.withUnsafeBytes { _ = CC_SHA1($0, CC_LONG(data.count), &digest) } + return Data(bytes: digest).base64EncodedString() + } +} + +private extension Data { + + init(buffer: UnsafeBufferPointer) { + self.init(bytes: buffer.baseAddress!, count: buffer.count) + } + +} + +private extension UnsafeBufferPointer { + + func fromOffset(_ offset: Int) -> UnsafeBufferPointer { + return UnsafeBufferPointer(start: baseAddress?.advanced(by: offset), count: count - offset) + } + +} + +private let emptyBuffer = UnsafeBufferPointer(start: nil, count: 0) + +#if swift(>=4) +#else +fileprivate extension String { + var count: Int { + return self.characters.count + } +} +#endif diff --git a/Sources/ApolloWebSocket/WebSocketClient.swift b/Sources/ApolloWebSocket/WebSocketClient.swift index 2cbb5d8b9d..a51e7cb8d9 100644 --- a/Sources/ApolloWebSocket/WebSocketClient.swift +++ b/Sources/ApolloWebSocket/WebSocketClient.swift @@ -9,6 +9,9 @@ public protocol WebSocketClient: AnyObject { var request: URLRequest { get set } /// The delegate that will receive networking event updates for this websocket client. + /// + /// - Note: The `WebSocketTransport` will set itself as the delgate for the client. Consumers + /// should set themselves as the delegate for the `WebSocketTransport` to observe events. var delegate: WebSocketClientDelegate? { get set } /// `DispatchQueue` where the websocket client should call all delegate callbacks. diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 208f7b7ae4..766643a159 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -46,6 +46,7 @@ public class WebSocketTransport { } var socketConnectionState = Atomic(.disconnected) + /// Indicates if the websocket connection has been acknowledge by the server. private var acked = false private var queue: [Int: String] = [:] From d9de5cd40a58aa4a49dfd5fd79731422957c7baa Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 15:41:10 -0700 Subject: [PATCH 02/14] Modernize unsafe data access in compression --- Apollo.xcodeproj/project.pbxproj | 4 ++ Sources/ApolloWebSocket/Compression.swift | 38 +++++++-------- Sources/ApolloWebSocket/WebSocket.swift | 20 ++++---- .../WebSocket/CompressionTests.swift | 48 +++++++++++++++++++ 4 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 Tests/ApolloTests/WebSocket/CompressionTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index c2d1c57011..fb36805a58 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -194,6 +194,7 @@ DE181A2E26C5C299000C0B9C /* SSLClientCertificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */; }; DE181A3026C5C38E000C0B9C /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */; }; DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; + DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; }; DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; @@ -751,6 +752,7 @@ DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLClientCertificate.swift; sourceTree = ""; }; DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLSecurity.swift; sourceTree = ""; }; DE181A3126C5C401000C0B9C /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compression.swift; sourceTree = ""; }; + DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = ""; }; DE3C7973260A646300D2F4FF /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSet.swift; sourceTree = ""; }; DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDict.swift; sourceTree = ""; }; @@ -1728,6 +1730,7 @@ 9B7BDA8923FDE92900ACD198 /* WebSocketTests.swift */, 9B7BDA8A23FDE92900ACD198 /* SplitNetworkTransportTests.swift */, D90F1AF92479DEE5007A1534 /* WebSocketTransportTests.swift */, + DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */, ); path = WebSocket; sourceTree = ""; @@ -2645,6 +2648,7 @@ 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, DED45DE9261B96B70086EF63 /* LoadQueryFromStoreTests.swift in Sources */, 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */, + DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */, 9F21735B2568F3E200566121 /* PossiblyDeferredTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift index a9f993f080..640da99cdc 100644 --- a/Sources/ApolloWebSocket/Compression.swift +++ b/Sources/ApolloWebSocket/Compression.swift @@ -14,10 +14,6 @@ import Foundation import zlib -enum CompressionError: Swift.Error { - case dataBufferEmpty -} - class Decompressor { private var strm = z_stream() private var buffer = [UInt8](repeating: 0, count: 0x2000) @@ -47,7 +43,7 @@ class Decompressor { func decompress(_ data: Data, finish: Bool) throws -> Data { return try data.withUnsafeBytes { pointer -> Data in guard let bytes = pointer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - throw CompressionError.dataBufferEmpty + return data } return try decompress(bytes: bytes, count: data.count, finish: finish) } @@ -72,13 +68,16 @@ class Decompressor { strm.avail_in = CUnsignedInt(count) repeat { - strm.next_out = UnsafeMutablePointer(&buffer) - strm.avail_out = CUnsignedInt(buffer.count) + buffer.withUnsafeMutableBytes { buffer in + let bytePtr = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self) + strm.next_out = UnsafeMutablePointer(bytePtr) + strm.avail_out = CUnsignedInt(buffer.count) - res = inflate(&strm, 0) + res = inflate(&strm, 0) - let byteCount = buffer.count - Int(strm.avail_out) - out.append(buffer, count: byteCount) + let byteCount = buffer.count - Int(strm.avail_out) + out.append(bytePtr, count: byteCount) + } } while res == Z_OK && strm.avail_out == 0 guard (res == Z_OK && strm.avail_out > 0) @@ -129,22 +128,23 @@ class Compressor { func compress(_ data: Data) throws -> Data { var compressed = Data() var res:CInt = 0 - try data.withUnsafeBytes { pointer -> Void in + data.withUnsafeBytes { pointer -> Void in guard let bytes = pointer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - throw CompressionError.dataBufferEmpty + return } strm.next_in = UnsafeMutablePointer(mutating: bytes) strm.avail_in = CUnsignedInt(data.count) repeat { - strm.next_out = UnsafeMutablePointer(&buffer) - strm.avail_out = CUnsignedInt(buffer.count) - - res = deflate(&strm, Z_SYNC_FLUSH) - - let byteCount = buffer.count - Int(strm.avail_out) - compressed.append(buffer, count: byteCount) + buffer.withUnsafeMutableBytes { buffer in + let bytePtr = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self) + strm.next_out = bytePtr + strm.avail_out = CUnsignedInt(buffer.count) + res = deflate(&strm, Z_SYNC_FLUSH) + let byteCount = buffer.count - Int(strm.avail_out) + compressed.append(bytePtr, count: byteCount) + } } while res == Z_OK && strm.avail_out == 0 diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 763886b0fd..59117d755f 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -31,17 +31,17 @@ public enum CloseCode : UInt16 { case messageTooBig = 1009 } -public enum ErrorType: Error { - case outputStreamWriteError //output stream error during write - case compressionError - case invalidSSLError //Invalid SSL certificate - case writeTimeoutError //The socket timed out waiting to be ready to write - case protocolError //There was an error parsing the WebSocket frames - case upgradeError //There was an error during the HTTP upgrade - case closeError //There was an error during the close (socket probably has been dereferenced) -} - public struct WSError: Error { + public enum ErrorType { + case outputStreamWriteError //output stream error during write + case compressionError // Error with compressing or decompressing data + case invalidSSLError //Invalid SSL certificate + case writeTimeoutError //The socket timed out waiting to be ready to write + case protocolError //There was an error parsing the WebSocket frames + case upgradeError //There was an error during the HTTP upgrade + case closeError //There was an error during the close (socket probably has been dereferenced) + } + public let type: ErrorType public let message: String public let code: Int diff --git a/Tests/ApolloTests/WebSocket/CompressionTests.swift b/Tests/ApolloTests/WebSocket/CompressionTests.swift new file mode 100644 index 0000000000..e1b3d20f89 --- /dev/null +++ b/Tests/ApolloTests/WebSocket/CompressionTests.swift @@ -0,0 +1,48 @@ +// Created by Joseph Ross on 7/16/14. +// Copyright © 2017 Joseph Ross. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) + +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + +// Compression implementation is implemented in conformance with RFC 7692 Compression Extensions +// for WebSocket: https://tools.ietf.org/html/rfc7692 + +import XCTest +@testable import ApolloWebSocket + +class CompressionTests: XCTestCase { + + func testBasic() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! + + let rawData = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!".data(using: .utf8)! + + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) + + XCTAssert(rawData == uncompressed) + } + + func testHugeData() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! + + // 2 Gigs! + var rawData = Data(repeating: 0, count: 0x80000) + let rawDataLen = rawData.count + rawData.withUnsafeMutableBytes { ptr -> Void in + arc4random_buf(ptr.baseAddress, rawDataLen) + } + + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) + + XCTAssert(rawData == uncompressed) + } + +} From f676a21048ae58252e523c72a25d0baccf6906e4 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 15:42:30 -0700 Subject: [PATCH 03/14] Minor cleanup of headers --- Sources/ApolloWebSocket/Compression.swift | 4 ++-- Sources/ApolloWebSocket/SSLClientCertificate.swift | 2 +- Sources/ApolloWebSocket/SSLSecurity.swift | 2 +- Sources/ApolloWebSocket/WebSocket.swift | 2 +- Tests/ApolloTests/WebSocket/CompressionTests.swift | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift index 640da99cdc..1c398e227f 100644 --- a/Sources/ApolloWebSocket/Compression.swift +++ b/Sources/ApolloWebSocket/Compression.swift @@ -4,10 +4,10 @@ // // This is a derived work derived from // Starscream(https://github.com/daltoniam/Starscream) - +// // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE - +// // Compression implementation is implemented in conformance with RFC 7692 Compression Extensions // for WebSocket: https://tools.ietf.org/html/rfc7692 diff --git a/Sources/ApolloWebSocket/SSLClientCertificate.swift b/Sources/ApolloWebSocket/SSLClientCertificate.swift index 21a6681ae0..810a5717fe 100644 --- a/Sources/ApolloWebSocket/SSLClientCertificate.swift +++ b/Sources/ApolloWebSocket/SSLClientCertificate.swift @@ -4,7 +4,7 @@ // // This is a derived work derived from // Starscream(https://github.com/daltoniam/Starscream) - +// // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/SSLSecurity.swift b/Sources/ApolloWebSocket/SSLSecurity.swift index 90aa8ee96a..2a386956c9 100644 --- a/Sources/ApolloWebSocket/SSLSecurity.swift +++ b/Sources/ApolloWebSocket/SSLSecurity.swift @@ -4,7 +4,7 @@ // // This is a derived work derived from // Starscream(https://github.com/daltoniam/Starscream) - +// // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 59117d755f..206f9c0496 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -4,7 +4,7 @@ // // This is a derived work derived from // Starscream(https://github.com/daltoniam/Starscream) - +// // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Tests/ApolloTests/WebSocket/CompressionTests.swift b/Tests/ApolloTests/WebSocket/CompressionTests.swift index e1b3d20f89..70a4051940 100644 --- a/Tests/ApolloTests/WebSocket/CompressionTests.swift +++ b/Tests/ApolloTests/WebSocket/CompressionTests.swift @@ -4,10 +4,10 @@ // // This is a derived work derived from // Starscream(https://github.com/daltoniam/Starscream) - +// // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE - +// // Compression implementation is implemented in conformance with RFC 7692 Compression Extensions // for WebSocket: https://tools.ietf.org/html/rfc7692 From 1e542e65561ddd154a360a7011ac901f7c1fe563 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 15:54:16 -0700 Subject: [PATCH 04/14] Clean up use of constants --- Sources/ApolloWebSocket/WebSocket.swift | 123 +++++++++++++----------- 1 file changed, 67 insertions(+), 56 deletions(-) diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 206f9c0496..66d52343fc 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -13,10 +13,6 @@ import Foundation import CoreFoundation import CommonCrypto -let WebsocketDidConnectNotification = "WebsocketDidConnectNotification" -let WebsocketDidDisconnectNotification = "WebsocketDidDisconnectNotification" -let WebsocketDisconnectionErrorKeyName = "WebsocketDisconnectionErrorKeyName" - //Standard WebSocket close codes public enum CloseCode : UInt16 { case normal = 1000 @@ -271,30 +267,36 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat // Where the callback is executed. It defaults to the main UI thread queue. public var callbackQueue = DispatchQueue.main - // MARK: - Constants - - let headerWSUpgradeName = "Upgrade" - let headerWSUpgradeValue = "websocket" - let headerWSHostName = "Host" - let headerWSConnectionName = "Connection" - let headerWSConnectionValue = "Upgrade" - let headerWSProtocolName = "Sec-WebSocket-Protocol" - let headerWSVersionName = "Sec-WebSocket-Version" - let headerWSVersionValue = "13" - let headerWSExtensionName = "Sec-WebSocket-Extensions" - let headerWSKeyName = "Sec-WebSocket-Key" - let headerOriginName = "Origin" - let headerWSAcceptName = "Sec-WebSocket-Accept" - let BUFFER_MAX = 4096 - let FinMask: UInt8 = 0x80 - let OpCodeMask: UInt8 = 0x0F - let RSVMask: UInt8 = 0x70 - let RSV1Mask: UInt8 = 0x40 - let MaskMask: UInt8 = 0x80 - let PayloadLenMask: UInt8 = 0x7F - let MaxFrameSize: Int = 32 - let httpSwitchProtocolCode = 101 - let supportedSSLSchemes = ["wss", "https"] + private struct Constants { + static let headerWSUpgradeName = "Upgrade" + static let headerWSUpgradeValue = "websocket" + static let headerWSHostName = "Host" + static let headerWSConnectionName = "Connection" + static let headerWSConnectionValue = "Upgrade" + static let headerWSProtocolName = "Sec-WebSocket-Protocol" + static let headerWSVersionName = "Sec-WebSocket-Version" + static let headerWSVersionValue = "13" + static let headerWSExtensionName = "Sec-WebSocket-Extensions" + static let headerWSKeyName = "Sec-WebSocket-Key" + static let headerOriginName = "Origin" + static let headerWSAcceptName = "Sec-WebSocket-Accept" + static let BUFFER_MAX = 4096 + static let FinMask: UInt8 = 0x80 + static let OpCodeMask: UInt8 = 0x0F + static let RSVMask: UInt8 = 0x70 + static let RSV1Mask: UInt8 = 0x40 + static let MaskMask: UInt8 = 0x80 + static let PayloadLenMask: UInt8 = 0x7F + static let MaxFrameSize: Int = 32 + static let httpSwitchProtocolCode = 101 + static let supportedSSLSchemes = ["wss", "https"] + static let WebsocketDisconnectionErrorKeyName = "WebsocketDisconnectionErrorKeyName" + + struct Notifications { + static let WebsocketDidConnect = "WebsocketDidConnectNotification" + static let WebsocketDidDisconnect = "WebsocketDidDisconnectNotification" + } + } public class WSResponse { var isFin = false @@ -380,17 +382,18 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat public init(request: URLRequest, protocols: [String]? = nil, stream: WSStream = FoundationStream()) { self.request = request self.stream = stream - if request.value(forHTTPHeaderField: headerOriginName) == nil { + if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil { guard let url = request.url else {return} var origin = url.absoluteString if let hostUrl = URL (string: "/", relativeTo: url) { origin = hostUrl.absoluteString origin.remove(at: origin.index(before: origin.endIndex)) } - self.request.setValue(origin, forHTTPHeaderField: headerOriginName) + self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } if let protocols = protocols, !protocols.isEmpty { - self.request.setValue(protocols.joined(separator: ","), forHTTPHeaderField: headerWSProtocolName) + self.request.setValue(protocols.joined(separator: ","), + forHTTPHeaderField: Constants.headerWSProtocolName) } writeQueue.maxConcurrentOperationCount = 1 } @@ -504,24 +507,30 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat guard let url = request.url else {return} var port = url.port if port == nil { - if supportedSSLSchemes.contains(url.scheme!) { + if Constants.supportedSSLSchemes.contains(url.scheme!) { port = 443 } else { port = 80 } } - request.setValue(headerWSUpgradeValue, forHTTPHeaderField: headerWSUpgradeName) - request.setValue(headerWSConnectionValue, forHTTPHeaderField: headerWSConnectionName) + request.setValue(Constants.headerWSUpgradeValue, + forHTTPHeaderField: Constants.headerWSUpgradeName) + request.setValue(Constants.headerWSConnectionValue, + forHTTPHeaderField: Constants.headerWSConnectionName) headerSecKey = generateWebSocketKey() - request.setValue(headerWSVersionValue, forHTTPHeaderField: headerWSVersionName) - request.setValue(headerSecKey, forHTTPHeaderField: headerWSKeyName) + request.setValue(Constants.headerWSVersionValue, + forHTTPHeaderField: Constants.headerWSVersionName) + request.setValue(headerSecKey, + forHTTPHeaderField: Constants.headerWSKeyName) if enableCompression { let val = "permessage-deflate; client_max_window_bits; server_max_window_bits=15" - request.setValue(val, forHTTPHeaderField: headerWSExtensionName) + request.setValue(val, forHTTPHeaderField: Constants.headerWSExtensionName) + } + + if request.allHTTPHeaderFields?[Constants.headerWSHostName] == nil { + request.setValue("\(url.host!):\(port!)", forHTTPHeaderField: Constants.headerWSHostName) } - let hostValue = request.allHTTPHeaderFields?[headerWSHostName] ?? "\(url.host!):\(port!)" - request.setValue(hostValue, forHTTPHeaderField: headerWSHostName) var path = url.absoluteString let offset = (url.scheme?.count ?? 2) + 3 @@ -574,7 +583,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat // Disconnect and clean up any existing streams before setting up a new pair disconnectStream(nil, runDelegate: false) - let useSSL = supportedSSLSchemes.contains(url.scheme!) + let useSSL = Constants.supportedSSLSchemes.contains(url.scheme!) #if os(Linux) let settings = SSLSettings(useSSL: useSSL, disableCertValidation: disableSSLCertValidation, @@ -759,7 +768,9 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat guard let self = self else { return } self.onConnect?() self.delegate?.websocketDidConnect(socket: self) - NotificationCenter.default.post(name: NSNotification.Name(WebsocketDidConnectNotification), object: self) + NotificationCenter.default + .post(name: NSNotification.Name(Constants.Notifications.WebsocketDidConnect), + object: self) } } //totalSize += 1 //skip the last \n @@ -798,15 +809,15 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat i += 1 } onHttpResponseHeaders?(headers) - if code != httpSwitchProtocolCode { + if code != Constants.httpSwitchProtocolCode { return code } - if let extensionHeader = headers[headerWSExtensionName.lowercased()] { + if let extensionHeader = headers[Constants.headerWSExtensionName.lowercased()] { processExtensionHeader(extensionHeader) } - if let acceptKey = headers[headerWSAcceptName.lowercased()] { + if let acceptKey = headers[Constants.headerWSAcceptName.lowercased()] { if acceptKey.count > 0 { if headerSecKey.count > 0 { let sha = "\(headerSecKey)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1Base64() @@ -909,16 +920,16 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat _ = processResponse(response) return buffer.fromOffset(bufferLen - extra) } else { - let isFin = (FinMask & baseAddress[0]) - let receivedOpcodeRawValue = (OpCodeMask & baseAddress[0]) + let isFin = (Constants.FinMask & baseAddress[0]) + let receivedOpcodeRawValue = (Constants.OpCodeMask & baseAddress[0]) let receivedOpcode = OpCode(rawValue: receivedOpcodeRawValue) - let isMasked = (MaskMask & baseAddress[1]) - let payloadLen = (PayloadLenMask & baseAddress[1]) + let isMasked = (Constants.MaskMask & baseAddress[1]) + let payloadLen = (Constants.PayloadLenMask & baseAddress[1]) var offset = 2 if compressionState.supportsCompression && receivedOpcode != .continueFrame { - compressionState.messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 + compressionState.messageNeedsDecompression = (Constants.RSV1Mask & baseAddress[0]) > 0 } - if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !compressionState.messageNeedsDecompression { + if (isMasked > 0 || (Constants.RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !compressionState.messageNeedsDecompression { let errCode = CloseCode.protocolError.rawValue doDisconnect(WSError(type: .protocolError, message: "masked and rsv data is not currently supported", code: Int(errCode))) writeError(errCode) @@ -1137,7 +1148,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat guard let self = self else { return } guard let sOperation = operation else { return } var offset = 2 - var firstByte:UInt8 = self.FinMask | code.rawValue + var firstByte:UInt8 = Constants.FinMask | code.rawValue var data = data if [.textFrame, .binaryFrame].contains(code), let compressor = self.compressionState.compressor { do { @@ -1145,13 +1156,13 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat if self.compressionState.clientNoContextTakeover { try compressor.reset() } - firstByte |= self.RSV1Mask + firstByte |= Constants.RSV1Mask } catch { // TODO: report error? We can just send the uncompressed frame. } } let dataLength = data.count - let frame = NSMutableData(capacity: dataLength + self.MaxFrameSize) + let frame = NSMutableData(capacity: dataLength + Constants.MaxFrameSize) let buffer = UnsafeMutableRawPointer(frame!.mutableBytes).assumingMemoryBound(to: UInt8.self) buffer[0] = firstByte if dataLength < 126 { @@ -1165,7 +1176,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat WebSocket.writeUint64(buffer, offset: offset, value: UInt64(dataLength)) offset += MemoryLayout.size } - buffer[1] |= self.MaskMask + buffer[1] |= Constants.MaskMask let maskKey = UnsafeMutablePointer(buffer + offset) _ = SecRandomCopyBytes(kSecRandomDefault, Int(MemoryLayout.size), maskKey) offset += MemoryLayout.size @@ -1218,8 +1229,8 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat guard let self = self else { return } self.onDisconnect?(error) self.delegate?.websocketDidDisconnect(socket: self, error: error) - let userInfo = error.map{ [WebsocketDisconnectionErrorKeyName: $0] } - NotificationCenter.default.post(name: NSNotification.Name(WebsocketDidDisconnectNotification), object: self, userInfo: userInfo) + let userInfo = error.map{ [Constants.WebsocketDisconnectionErrorKeyName: $0] } + NotificationCenter.default.post(name: NSNotification.Name(Constants.Notifications.WebsocketDidDisconnect), object: self, userInfo: userInfo) } } From 32c3ce5997cf336dcece8cef4319576c81850007 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 16:16:07 -0700 Subject: [PATCH 05/14] Break Stream into new file --- Apollo.xcodeproj/project.pbxproj | 6 + Sources/ApolloWebSocket/WebSocket.swift | 209 +---------------- Sources/ApolloWebSocket/WebSocketStream.swift | 212 ++++++++++++++++++ 3 files changed, 225 insertions(+), 202 deletions(-) create mode 100644 Sources/ApolloWebSocket/WebSocketStream.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index fb36805a58..5cdce8711b 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -195,6 +195,8 @@ DE181A3026C5C38E000C0B9C /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */; }; DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; }; + DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; + DE181A3726C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; @@ -753,6 +755,7 @@ DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLSecurity.swift; sourceTree = ""; }; DE181A3126C5C401000C0B9C /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compression.swift; sourceTree = ""; }; DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = ""; }; + DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketStream.swift; sourceTree = ""; }; DE3C7973260A646300D2F4FF /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSet.swift; sourceTree = ""; }; DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDict.swift; sourceTree = ""; }; @@ -1177,6 +1180,7 @@ children = ( 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */, DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */, + DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */, 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */, 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */, 9B7BDA9423FDE94C00ACD198 /* WebSocketError.swift */, @@ -2463,6 +2467,7 @@ 9B7BDA9B23FDE94C00ACD198 /* WebSocketError.swift in Sources */, 9B7BDA9D23FDE94C00ACD198 /* SplitNetworkTransport.swift in Sources */, 9B7BDA9E23FDE94C00ACD198 /* OperationMessage.swift in Sources */, + DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */, DE181A3226C5C401000C0B9C /* Compression.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2643,6 +2648,7 @@ C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */, F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, + DE181A3726C5DE4F000C0B9C /* WebSocketStream.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 66d52343fc..c4a771d9df 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -8,9 +8,7 @@ // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE - import Foundation -import CoreFoundation import CommonCrypto //Standard WebSocket close codes @@ -56,200 +54,9 @@ public struct SSLSettings { #endif } -public protocol WSStreamDelegate: class { - func newBytesInStream() - func streamDidError(error: Error?) -} - -//This protocol is to allow custom implemention of the underlining stream. This way custom socket libraries (e.g. linux) can be used -public protocol WSStream { - var delegate: WSStreamDelegate? {get set} - func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping ((Error?) -> Void)) - func write(data: Data) -> Int - func read() -> Data? - func cleanup() - #if os(Linux) || os(watchOS) - #else - func sslTrust() -> (trust: SecTrust?, domain: String?) - #endif -} - -open class FoundationStream : NSObject, WSStream, StreamDelegate { - private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) - private var inputStream: InputStream? - private var outputStream: OutputStream? - public weak var delegate: WSStreamDelegate? - let BUFFER_MAX = 4096 - - public var enableSOCKSProxy = false - - public func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping ((Error?) -> Void)) { - var readStream: Unmanaged? - var writeStream: Unmanaged? - let h = url.host! as NSString - CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream) - inputStream = readStream!.takeRetainedValue() - outputStream = writeStream!.takeRetainedValue() - - #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work - #else - if enableSOCKSProxy { - let proxyDict = CFNetworkCopySystemProxySettings() - let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue()) - let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy) - CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig) - CFReadStreamSetProperty(inputStream, propertyKey, socksConfig) - } - #endif - - guard let inStream = inputStream, let outStream = outputStream else { return } - inStream.delegate = self - outStream.delegate = self - if ssl.useSSL { - inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) - outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) - #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work - #else - var settings = [NSObject: NSObject]() - if ssl.disableCertValidation { - settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: false) - } - if ssl.overrideTrustHostname { - if let hostname = ssl.desiredTrustHostname { - settings[kCFStreamSSLPeerName] = hostname as NSString - } else { - settings[kCFStreamSSLPeerName] = kCFNull - } - } - if let sslClientCertificate = ssl.sslClientCertificate { - settings[kCFStreamSSLCertificates] = sslClientCertificate.streamSSLCertificates - } - - inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) - outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) - #endif - - #if os(Linux) - #else - if let cipherSuites = ssl.cipherSuites { - #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work - #else - if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?, - let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? { - let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) - let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) - if resIn != errSecSuccess { - completion(WSError(type: .invalidSSLError, message: "Error setting ingoing cypher suites", code: Int(resIn))) - } - if resOut != errSecSuccess { - completion(WSError(type: .invalidSSLError, message: "Error setting outgoing cypher suites", code: Int(resOut))) - } - } - #endif - } - #endif - } - - CFReadStreamSetDispatchQueue(inStream, workQueue) - CFWriteStreamSetDispatchQueue(outStream, workQueue) - inStream.open() - outStream.open() - - var out = timeout// wait X seconds before giving up - workQueue.async { [weak self] in - while !outStream.hasSpaceAvailable { - usleep(100) // wait until the socket is ready - out -= 100 - if out < 0 { - completion(WSError(type: .writeTimeoutError, message: "Timed out waiting for the socket to be ready for a write", code: 0)) - return - } else if let error = outStream.streamError { - completion(error) - return // disconnectStream will be called. - } else if self == nil { - completion(WSError(type: .closeError, message: "socket object has been dereferenced", code: 0)) - return - } - } - completion(nil) //success! - } - } - - public func write(data: Data) -> Int { - guard let outStream = outputStream else {return -1} - let buffer = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self) - return outStream.write(buffer, maxLength: data.count) - } - - public func read() -> Data? { - guard let stream = inputStream else {return nil} - let buf = NSMutableData(capacity: BUFFER_MAX) - let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) - let length = stream.read(buffer, maxLength: BUFFER_MAX) - if length < 1 { - return nil - } - return Data(bytes: buffer, count: length) - } - - public func cleanup() { - if let stream = inputStream { - stream.delegate = nil - CFReadStreamSetDispatchQueue(stream, nil) - stream.close() - } - if let stream = outputStream { - stream.delegate = nil - CFWriteStreamSetDispatchQueue(stream, nil) - stream.close() - } - outputStream = nil - inputStream = nil - } - - #if os(Linux) || os(watchOS) - #else - public func sslTrust() -> (trust: SecTrust?, domain: String?) { - 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) in - SSLGetPeerDomainName(sslContextOut, peerNamePtr, &peerNameLen) - } - if let peerDomain = String(bytes: peerName, encoding: .utf8), peerDomain.count > 0 { - domain = peerDomain - } - } - - return (trust, domain) - } - #endif - - /** - Delegate for the stream methods. Processes incoming bytes - */ - open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - if eventCode == .hasBytesAvailable { - if aStream == inputStream { - delegate?.newBytesInStream() - } - } else if eventCode == .errorOccurred { - delegate?.streamDidError(error: aStream.streamError) - } else if eventCode == .endEncountered { - delegate?.streamDidError(error: nil) - } - } -} - //WebSocket implementation -open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegate { +open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStreamDelegate { public enum OpCode : UInt8 { case continueFrame = 0x0 @@ -262,11 +69,6 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat // B-F reserved. } - public static let ErrorDomain = "WebSocket" - - // Where the callback is executed. It defaults to the main UI thread queue. - public var callbackQueue = DispatchQueue.main - private struct Constants { static let headerWSUpgradeName = "Upgrade" static let headerWSUpgradeValue = "websocket" @@ -315,6 +117,9 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat /// and also connection/disconnect messages. public weak var delegate: WebSocketClientDelegate? + // Where the callback is executed. It defaults to the main UI thread queue. + public var callbackQueue = DispatchQueue.main + public var onConnect: (() -> Void)? public var onDisconnect: ((Error?) -> Void)? public var onText: ((String) -> Void)? @@ -358,7 +163,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat var compressor:Compressor? = nil } - private var stream: WSStream + private var stream: WebSocketStream private var connected = false private var isConnecting = false private let mutex = NSLock() @@ -379,9 +184,9 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WSStreamDelegat } /// Used for setting protocols. - public init(request: URLRequest, protocols: [String]? = nil, stream: WSStream = FoundationStream()) { + public init(request: URLRequest, protocols: [String]? = nil) { self.request = request - self.stream = stream + self.stream = FoundationStream() if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil { guard let url = request.url else {return} var origin = url.absoluteString diff --git a/Sources/ApolloWebSocket/WebSocketStream.swift b/Sources/ApolloWebSocket/WebSocketStream.swift new file mode 100644 index 0000000000..b56e7936d5 --- /dev/null +++ b/Sources/ApolloWebSocket/WebSocketStream.swift @@ -0,0 +1,212 @@ +// Created by Dalton Cherry on 7/16/14. +// Copyright (c) 2014-2017 Dalton Cherry. +// Modified by Anthony Miller & Apollo GraphQL on 8/12/21 +// +// This is a derived work derived from +// Starscream(https://github.com/daltoniam/Starscream) +// +// Original Work License: http://www.apache.org/licenses/LICENSE-2.0 +// Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE + +import Foundation + +protocol WebSocketStreamDelegate: AnyObject { + func newBytesInStream() + func streamDidError(error: Error?) +} + +// This protocol is to allow custom implemention of the underlining stream. +// This way custom socket libraries (e.g. linux) can be used +protocol WebSocketStream { + var delegate: WebSocketStreamDelegate? { get set } + + func connect(url: URL, + port: Int, + timeout: TimeInterval, + ssl: SSLSettings, + completion: @escaping ((Error?) -> Void)) + + func write(data: Data) -> Int + func read() -> Data? + func cleanup() + + #if os(Linux) || os(watchOS) + #else + func sslTrust() -> (trust: SecTrust?, domain: String?) + #endif +} + +class FoundationStream : NSObject, WebSocketStream, StreamDelegate { + private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) + private var inputStream: InputStream? + private var outputStream: OutputStream? + weak var delegate: WebSocketStreamDelegate? + let BUFFER_MAX = 4096 + + var enableSOCKSProxy = false + + func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping ((Error?) -> Void)) { + var readStream: Unmanaged? + var writeStream: Unmanaged? + let h = url.host! as NSString + CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream) + inputStream = readStream!.takeRetainedValue() + outputStream = writeStream!.takeRetainedValue() + + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + if enableSOCKSProxy { + let proxyDict = CFNetworkCopySystemProxySettings() + let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue()) + let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy) + CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig) + CFReadStreamSetProperty(inputStream, propertyKey, socksConfig) + } + #endif + + guard let inStream = inputStream, let outStream = outputStream else { return } + inStream.delegate = self + outStream.delegate = self + if ssl.useSSL { + inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + var settings = [NSObject: NSObject]() + if ssl.disableCertValidation { + settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: false) + } + if ssl.overrideTrustHostname { + if let hostname = ssl.desiredTrustHostname { + settings[kCFStreamSSLPeerName] = hostname as NSString + } else { + settings[kCFStreamSSLPeerName] = kCFNull + } + } + if let sslClientCertificate = ssl.sslClientCertificate { + settings[kCFStreamSSLCertificates] = sslClientCertificate.streamSSLCertificates + } + + inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + #endif + + #if os(Linux) + #else + if let cipherSuites = ssl.cipherSuites { + #if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work + #else + if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?, + let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? { + let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) + let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) + if resIn != errSecSuccess { + completion(WSError(type: .invalidSSLError, message: "Error setting ingoing cypher suites", code: Int(resIn))) + } + if resOut != errSecSuccess { + completion(WSError(type: .invalidSSLError, message: "Error setting outgoing cypher suites", code: Int(resOut))) + } + } + #endif + } + #endif + } + + CFReadStreamSetDispatchQueue(inStream, workQueue) + CFWriteStreamSetDispatchQueue(outStream, workQueue) + inStream.open() + outStream.open() + + var out = timeout// wait X seconds before giving up + workQueue.async { [weak self] in + while !outStream.hasSpaceAvailable { + usleep(100) // wait until the socket is ready + out -= 100 + if out < 0 { + completion(WSError(type: .writeTimeoutError, message: "Timed out waiting for the socket to be ready for a write", code: 0)) + return + } else if let error = outStream.streamError { + completion(error) + return // disconnectStream will be called. + } else if self == nil { + completion(WSError(type: .closeError, message: "socket object has been dereferenced", code: 0)) + return + } + } + completion(nil) //success! + } + } + + func write(data: Data) -> Int { + guard let outStream = outputStream else {return -1} + let buffer = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self) + return outStream.write(buffer, maxLength: data.count) + } + + func read() -> Data? { + guard let stream = inputStream else {return nil} + let buf = NSMutableData(capacity: BUFFER_MAX) + let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) + let length = stream.read(buffer, maxLength: BUFFER_MAX) + if length < 1 { + return nil + } + return Data(bytes: buffer, count: length) + } + + func cleanup() { + if let stream = inputStream { + stream.delegate = nil + CFReadStreamSetDispatchQueue(stream, nil) + stream.close() + } + if let stream = outputStream { + stream.delegate = nil + CFWriteStreamSetDispatchQueue(stream, nil) + stream.close() + } + outputStream = nil + inputStream = nil + } + + #if os(Linux) || os(watchOS) + #else + func sslTrust() -> (trust: SecTrust?, domain: String?) { + 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 { ptr in + guard let ptr = ptr.baseAddress?.assumingMemoryBound(to: Int8.self) else { return } + SSLGetPeerDomainName(sslContextOut, ptr, &peerNameLen) + } + + if let peerDomain = String(bytes: peerName, encoding: .utf8), peerDomain.count > 0 { + domain = peerDomain + } + } + + return (trust, domain) + } + #endif + + /** + Delegate for the stream methods. Processes incoming bytes + */ + open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + if eventCode == .hasBytesAvailable { + if aStream == inputStream { + delegate?.newBytesInStream() + } + } else if eventCode == .errorOccurred { + delegate?.streamDidError(error: aStream.streamError) + } else if eventCode == .endEncountered { + delegate?.streamDidError(error: nil) + } + } +} From 94686460b796f26114af130b9558fa16023d1da2 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 12 Aug 2021 16:37:20 -0700 Subject: [PATCH 06/14] Cleanup more unsafe code warnings --- Apollo.xcodeproj/project.pbxproj | 2 - Sources/ApolloWebSocket/WebSocket.swift | 6 +-- .../WebSocket/CompressionTests.swift | 42 +++++++++---------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 5cdce8711b..39cf7d2c98 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -196,7 +196,6 @@ DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; }; DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; - DE181A3726C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; @@ -2648,7 +2647,6 @@ C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */, F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, - DE181A3726C5DE4F000C0B9C /* WebSocketStream.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index c4a771d9df..7dbb4e8071 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -1051,12 +1051,12 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream } -private extension String { +extension String { func sha1Base64() -> String { let data = self.data(using: String.Encoding.utf8)! var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) - data.withUnsafeBytes { _ = CC_SHA1($0, CC_LONG(data.count), &digest) } - return Data(bytes: digest).base64EncodedString() + data.withUnsafeBytes { _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest) } + return Data(digest).base64EncodedString() } } diff --git a/Tests/ApolloTests/WebSocket/CompressionTests.swift b/Tests/ApolloTests/WebSocket/CompressionTests.swift index 70a4051940..fc774a3d5b 100644 --- a/Tests/ApolloTests/WebSocket/CompressionTests.swift +++ b/Tests/ApolloTests/WebSocket/CompressionTests.swift @@ -16,33 +16,33 @@ import XCTest class CompressionTests: XCTestCase { - func testBasic() { - let compressor = Compressor(windowBits: 15)! - let decompressor = Decompressor(windowBits: 15)! + func testBasic() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! - let rawData = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!".data(using: .utf8)! + let rawData = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!".data(using: .utf8)! - let compressed = try! compressor.compress(rawData) - let uncompressed = try! decompressor.decompress(compressed, finish: true) + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) - XCTAssert(rawData == uncompressed) - } + XCTAssertEqual(rawData, uncompressed) + } - func testHugeData() { - let compressor = Compressor(windowBits: 15)! - let decompressor = Decompressor(windowBits: 15)! + func testHugeData() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! - // 2 Gigs! - var rawData = Data(repeating: 0, count: 0x80000) - let rawDataLen = rawData.count - rawData.withUnsafeMutableBytes { ptr -> Void in - arc4random_buf(ptr.baseAddress, rawDataLen) - } + // 2 Gigs! + var rawData = Data(repeating: 0, count: 0x80000) + let rawDataLen = rawData.count + rawData.withUnsafeMutableBytes { ptr -> Void in + arc4random_buf(ptr.baseAddress, rawDataLen) + } - let compressed = try! compressor.compress(rawData) - let uncompressed = try! decompressor.decompress(compressed, finish: true) + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) - XCTAssert(rawData == uncompressed) - } + XCTAssertEqual(rawData, uncompressed) + } } From afdbb2fd41e3af7b030c79d31397de617aa1598b Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 13 Aug 2021 11:11:54 -0700 Subject: [PATCH 07/14] Cleanup --- Sources/ApolloWebSocket/OperationMessage.swift | 3 +-- Sources/ApolloWebSocket/WebSocket.swift | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index 3d5567a7ec..0b8b308008 100644 --- a/Sources/ApolloWebSocket/OperationMessage.swift +++ b/Sources/ApolloWebSocket/OperationMessage.swift @@ -76,7 +76,7 @@ final class OperationMessage { var payload : JSONObject? do { - let json = try JSONSerializationFormat.deserialize(data: data ) as? JSONObject + let json = try serializationFormat.deserialize(data: data) as? JSONObject id = json?["id"] as? String type = json?["type"] as? String @@ -99,7 +99,6 @@ final class OperationMessage { } struct ParseHandler { - let type: String? let id: String? let payload: JSONObject? diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 7dbb4e8071..8f441fa804 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -12,7 +12,7 @@ import Foundation import CommonCrypto //Standard WebSocket close codes -public enum CloseCode : UInt16 { +enum CloseCode : UInt16 { case normal = 1000 case goingAway = 1001 case protocolError = 1002 @@ -56,7 +56,7 @@ public struct SSLSettings { //WebSocket implementation -open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStreamDelegate { +public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStreamDelegate { public enum OpCode : UInt8 { case continueFrame = 0x0 @@ -218,7 +218,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream /** Connect to the WebSocket server on a background thread. */ - open func connect() { + public func connect() { guard !isConnecting else { return } didDisconnect = false isConnecting = true @@ -235,7 +235,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream - Parameter forceTimeout: Maximum time to wait for the server to close the socket. - Parameter closeCode: The code to send on disconnect. The default is the normal close code for cleanly disconnecting a webSocket. */ - open func disconnect( + func disconnect( forceTimeout: TimeInterval? = nil, closeCode: UInt16 = CloseCode.normal.rawValue ) { @@ -267,7 +267,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream - parameter string: The string to write. - parameter completion: The (optional) completion handler. */ - open func write(string: String, completion: (() -> ())? = nil) { + func write(string: String, completion: (() -> ())? = nil) { guard isConnected else { return } dequeueWrite(string.data(using: String.Encoding.utf8)!, code: .textFrame, writeCompletion: completion) } @@ -284,7 +284,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream - parameter data: The data to write. - parameter completion: The (optional) completion handler. */ - open func write(data: Data, completion: (() -> ())? = nil) { + func write(data: Data, completion: (() -> ())? = nil) { guard isConnected else { return } dequeueWrite(data, code: .binaryFrame, writeCompletion: completion) } @@ -292,7 +292,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream /** Write a ping to the websocket. This sends it as a control frame. */ - open func write(ping: Data, completion: (() -> ())? = nil) { + public func write(ping: Data, completion: (() -> ())? = nil) { guard isConnected else { return } dequeueWrite(ping, code: .ping, writeCompletion: completion) } @@ -300,7 +300,7 @@ open class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSocketStream /** Write a pong to the websocket. This sends it as a control frame. */ - open func write(pong: Data, completion: (() -> ())? = nil) { + func write(pong: Data, completion: (() -> ())? = nil) { guard isConnected else { return } dequeueWrite(pong, code: .pong, writeCompletion: completion) } From 2655985223af4784470f9366f44c95175dfb4839 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 13 Aug 2021 11:24:06 -0700 Subject: [PATCH 08/14] Fix scoping of WSError --- Sources/ApolloWebSocket/Compression.swift | 18 ++++++++--- Sources/ApolloWebSocket/WebSocket.swift | 31 +++++++++---------- Sources/ApolloWebSocket/WebSocketError.swift | 3 ++ Sources/ApolloWebSocket/WebSocketStream.swift | 23 +++++++++++--- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift index 1c398e227f..e843095b77 100644 --- a/Sources/ApolloWebSocket/Compression.swift +++ b/Sources/ApolloWebSocket/Compression.swift @@ -15,6 +15,11 @@ import Foundation import zlib class Decompressor { + enum Error: Swift.Error { + case resetFailed + case decompressionFailed + } + private var strm = z_stream() private var buffer = [UInt8](repeating: 0, count: 0x2000) private var inflateInitialized = false @@ -37,7 +42,7 @@ class Decompressor { func reset() throws { teardownInflate() - guard initInflate() else { throw WSError(type: .compressionError, message: "Error for decompressor on reset", code: 0) } + guard initInflate() else { throw Error.resetFailed } } func decompress(_ data: Data, finish: Bool) throws -> Data { @@ -83,7 +88,7 @@ class Decompressor { guard (res == Z_OK && strm.avail_out > 0) || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) else { - throw WSError(type: .compressionError, message: "Error on decompressing", code: 0) + throw Error.decompressionFailed } } @@ -99,6 +104,11 @@ class Decompressor { } class Compressor { + enum Error: Swift.Error { + case resetFailed + case compressionFailed + } + private var strm = z_stream() private var buffer = [UInt8](repeating: 0, count: 0x2000) private var deflateInitialized = false @@ -122,7 +132,7 @@ class Compressor { func reset() throws { teardownDeflate() - guard initDeflate() else { throw WSError(type: .compressionError, message: "Error for compressor on reset", code: 0) } + guard initDeflate() else { throw Error.resetFailed } } func compress(_ data: Data) throws -> Data { @@ -153,7 +163,7 @@ class Compressor { guard res == Z_OK && strm.avail_out > 0 || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) else { - throw WSError(type: .compressionError, message: "Error on compressing", code: 0) + throw Error.compressionFailed } compressed.removeLast(4) diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 8f441fa804..55aeab1faf 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -25,22 +25,6 @@ enum CloseCode : UInt16 { case messageTooBig = 1009 } -public struct WSError: Error { - public enum ErrorType { - case outputStreamWriteError //output stream error during write - case compressionError // Error with compressing or decompressing data - case invalidSSLError //Invalid SSL certificate - case writeTimeoutError //The socket timed out waiting to be ready to write - case protocolError //There was an error parsing the WebSocket frames - case upgradeError //There was an error during the HTTP upgrade - case closeError //There was an error during the close (socket probably has been dereferenced) - } - - public let type: ErrorType - public let message: String - public let code: Int -} - //SSL settings for the stream public struct SSLSettings { public let useSSL: Bool @@ -69,6 +53,21 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock // B-F reserved. } + public struct WSError: Swift.Error { + public enum ErrorType { + case outputStreamWriteError //output stream error during write + case invalidSSLError //Invalid SSL certificate + case writeTimeoutError //The socket timed out waiting to be ready to write + case protocolError //There was an error parsing the WebSocket frames + case upgradeError //There was an error during the HTTP upgrade + case closeError //There was an error during the close (socket probably has been dereferenced) + } + + public let type: ErrorType + public let message: String + public let code: Int + } + private struct Constants { static let headerWSUpgradeName = "Upgrade" static let headerWSUpgradeValue = "websocket" diff --git a/Sources/ApolloWebSocket/WebSocketError.swift b/Sources/ApolloWebSocket/WebSocketError.swift index 0f5e7996c1..1cc6046bb1 100644 --- a/Sources/ApolloWebSocket/WebSocketError.swift +++ b/Sources/ApolloWebSocket/WebSocketError.swift @@ -11,6 +11,7 @@ public struct WebSocketError: Error, LocalizedError { case unprocessedMessage(String) case serializedMessageError case neitherErrorNorPayloadReceived + case upgradeError(code: Int) var description: String { switch self { @@ -24,6 +25,8 @@ public struct WebSocketError: Error, LocalizedError { return "Websocket error: Serialized message not found" case .neitherErrorNorPayloadReceived: return "Websocket error: Did not receive an error or a payload." + case .upgradeError: + return "Websocket error: Invalid HTTP upgrade." } } } diff --git a/Sources/ApolloWebSocket/WebSocketStream.swift b/Sources/ApolloWebSocket/WebSocketStream.swift index b56e7936d5..14000e348f 100644 --- a/Sources/ApolloWebSocket/WebSocketStream.swift +++ b/Sources/ApolloWebSocket/WebSocketStream.swift @@ -101,10 +101,16 @@ class FoundationStream : NSObject, WebSocketStream, StreamDelegate { let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) if resIn != errSecSuccess { - completion(WSError(type: .invalidSSLError, message: "Error setting ingoing cypher suites", code: Int(resIn))) + completion(WebSocket.WSError( + type: .invalidSSLError, + message: "Error setting ingoing cypher suites", + code: Int(resIn))) } if resOut != errSecSuccess { - completion(WSError(type: .invalidSSLError, message: "Error setting outgoing cypher suites", code: Int(resOut))) + completion(WebSocket.WSError( + type: .invalidSSLError, + message: "Error setting outgoing cypher suites", + code: Int(resOut))) } } #endif @@ -123,13 +129,22 @@ class FoundationStream : NSObject, WebSocketStream, StreamDelegate { usleep(100) // wait until the socket is ready out -= 100 if out < 0 { - completion(WSError(type: .writeTimeoutError, message: "Timed out waiting for the socket to be ready for a write", code: 0)) + completion( + WebSocket.WSError( + type: .writeTimeoutError, + message: "Timed out waiting for the socket to be ready for a write", + code: 0)) return + } else if let error = outStream.streamError { completion(error) return // disconnectStream will be called. + } else if self == nil { - completion(WSError(type: .closeError, message: "socket object has been dereferenced", code: 0)) + completion(WebSocket.WSError( + type: .closeError, + message: "socket object has been dereferenced", + code: 0)) return } } From 3a01590a658d146eb077238692ed256a3f166dd2 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 13 Aug 2021 12:38:09 -0700 Subject: [PATCH 09/14] Remove Starscream dependency --- Apollo.podspec | 1 - Apollo.xcodeproj/project.pbxproj | 41 ------------------- .../xcshareddata/swiftpm/Package.resolved | 9 ---- Package.swift | 8 +--- Sources/ApolloWebSocket/Compression.swift | 2 +- .../SSLClientCertificate.swift | 6 +-- Sources/ApolloWebSocket/WebSocket.swift | 2 +- Sources/ApolloWebSocket/WebSocketStream.swift | 2 +- Sources/ApolloWebSocket/WebSocketTask.swift | 1 - .../ApolloWebSocket/WebSocketTransport.swift | 2 +- SwiftScripts/Package.resolved | 9 ---- 11 files changed, 9 insertions(+), 74 deletions(-) diff --git a/Apollo.podspec b/Apollo.podspec index d4b03a2254..30c1224c24 100644 --- a/Apollo.podspec +++ b/Apollo.podspec @@ -40,7 +40,6 @@ Pod::Spec.new do |s| s.subspec 'WebSocket' do |ss| ss.source_files = 'Sources/ApolloWebSocket/*.swift' ss.dependency 'Apollo/Core' - ss.dependency 'Apollo-Starscream', '~>3.1.2' end end diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 39cf7d2c98..54877e025a 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -196,7 +196,6 @@ DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; }; DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; - DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; DE674D9D261CEEE4000E8FC8 /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9B2061172591B3550020D1E0 /* c.txt */; }; @@ -841,7 +840,6 @@ buildActionMask = 2147483647; files = ( 9B7BDAFD23FDEE9300ACD198 /* Apollo.framework in Frameworks */, - DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1898,12 +1896,10 @@ buildRules = ( ); dependencies = ( - 9B7BDAFF23FDEF9E00ACD198 /* PBXTargetDependency */, 9B7BDAFC23FDEE9000ACD198 /* PBXTargetDependency */, ); name = ApolloWebSocket; packageProductDependencies = ( - DE3A2815268BCE6700A1BDC8 /* Starscream */, ); productName = ApolloWebSocket; productReference = 9B7BDA7D23FDE90400ACD198 /* ApolloWebSocket.framework */; @@ -2033,7 +2029,6 @@ ); name = Apollo; packageProductDependencies = ( - DE8C84F3268BBF8000C54D02 /* Starscream */, ); productName = Apollo; productReference = 9FC750441D2A532C00458D91 /* Apollo.framework */; @@ -2221,7 +2216,6 @@ mainGroup = 9FC7503A1D2A532C00458D91; packageReferences = ( 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */, - DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */, ); productRefGroup = 9FC750451D2A532C00458D91 /* Products */; projectDirPath = ""; @@ -2735,10 +2729,6 @@ target = 9FC750431D2A532C00458D91 /* Apollo */; targetProxy = 9B7BDAFB23FDEE9000ACD198 /* PBXContainerItemProxy */; }; - 9B7BDAFF23FDEF9E00ACD198 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 9B7BDAFE23FDEF9E00ACD198 /* Starscream */; - }; 9B7BDB1723FDF10300ACD198 /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = 9B7BDB1623FDF10300ACD198 /* SQLite */; @@ -3382,14 +3372,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 9B7BDAAA23FDEA7B00ACD198 /* XCRemoteSwiftPackageReference "Starscream" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/daltoniam/Starscream.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 3.1.1; - }; - }; 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stephencelis/SQLite.swift.git"; @@ -3398,14 +3380,6 @@ minimumVersion = 0.12.2; }; }; - DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apollographql/Starscream.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 3.1.2; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3414,26 +3388,11 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; - 9B7BDAFE23FDEF9E00ACD198 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = 9B7BDAAA23FDEA7B00ACD198 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; 9B7BDB1623FDF10300ACD198 /* SQLite */ = { isa = XCSwiftPackageProductDependency; package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; - DE3A2815268BCE6700A1BDC8 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; - DE8C84F3268BBF8000C54D02 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; DED45E95261B9EE30086EF63 /* SQLite */ = { isa = XCSwiftPackageProductDependency; package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; diff --git a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72f493cc0c..e99ab22147 100644 --- a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,15 +9,6 @@ "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", "version": "0.12.2" } - }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/apollographql/Starscream.git", - "state": { - "branch": null, - "revision": "8cf77babe5901693396436f4f418a6db0f328b78", - "version": "3.1.2" - } } ] }, diff --git a/Package.swift b/Package.swift index 6862f29fd4..b1214df847 100644 --- a/Package.swift +++ b/Package.swift @@ -38,10 +38,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/stephencelis/SQLite.swift.git", - .upToNextMinor(from: "0.12.2")), - .package( - url: "https://github.com/apollographql/Starscream", - .upToNextMinor(from: "3.1.2")), + .upToNextMinor(from: "0.12.2")) ], targets: [ .target( @@ -92,8 +89,7 @@ let package = Package( name: "ApolloWebSocket", dependencies: [ "Apollo", - "ApolloUtils", - .product(name: "Starscream", package: "Starscream"), + "ApolloUtils" ], exclude: [ "Info.plist" diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift index e843095b77..ca3f330dc5 100644 --- a/Sources/ApolloWebSocket/Compression.swift +++ b/Sources/ApolloWebSocket/Compression.swift @@ -59,7 +59,7 @@ class Decompressor { try decompress(bytes: bytes, count: count, out: &decompressed) if finish { - let tail:[UInt8] = [0x00, 0x00, 0xFF, 0xFF] + let tail: [UInt8] = [0x00, 0x00, 0xFF, 0xFF] try decompress(bytes: tail, count: tail.count, out: &decompressed) } diff --git a/Sources/ApolloWebSocket/SSLClientCertificate.swift b/Sources/ApolloWebSocket/SSLClientCertificate.swift index 810a5717fe..7807ee310b 100644 --- a/Sources/ApolloWebSocket/SSLClientCertificate.swift +++ b/Sources/ApolloWebSocket/SSLClientCertificate.swift @@ -73,17 +73,17 @@ public class SSLClientCertificate { let importStatus = SecPKCS12Import(pkcs12CFData, importOptions, &rawIdentitiesAndCertificates) guard importStatus == errSecSuccess else { - throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecPKCS12Import', see 'SecBase.h' - OSStatus: \(importStatus)") + throw SSLClientCertificateError(errorDescription: "Error during 'SecPKCS12Import', see 'SecBase.h' - OSStatus: \(importStatus)") } guard let identitiyAndCertificate = (rawIdentitiesAndCertificates as? Array>)?.first else { - throw SSLClientCertificateError(errorDescription: "(Starscream) Error - PKCS12 file is empty") + throw SSLClientCertificateError(errorDescription: "Error - PKCS12 file is empty") } let identity = identitiyAndCertificate[kSecImportItemIdentity as String] as! SecIdentity var identityCertificate: SecCertificate? let copyStatus = SecIdentityCopyCertificate(identity, &identityCertificate) guard copyStatus == errSecSuccess else { - throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecIdentityCopyCertificate', see 'SecBase.h' - OSStatus: \(copyStatus)") + throw SSLClientCertificateError(errorDescription: "Error during 'SecIdentityCopyCertificate', see 'SecBase.h' - OSStatus: \(copyStatus)") } self.streamSSLCertificates = NSArray(objects: identity, identityCertificate!) } catch { diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 55aeab1faf..4dbd69f494 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -99,7 +99,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock } } - public class WSResponse { + class WSResponse { var isFin = false public var code: OpCode = .continueFrame var bytesLeft = 0 diff --git a/Sources/ApolloWebSocket/WebSocketStream.swift b/Sources/ApolloWebSocket/WebSocketStream.swift index 14000e348f..377f0ffe2b 100644 --- a/Sources/ApolloWebSocket/WebSocketStream.swift +++ b/Sources/ApolloWebSocket/WebSocketStream.swift @@ -37,7 +37,7 @@ protocol WebSocketStream { } class FoundationStream : NSObject, WebSocketStream, StreamDelegate { - private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) + private let workQueue = DispatchQueue(label: "com.apollographql.websocket", attributes: []) private var inputStream: InputStream? private var outputStream: OutputStream? weak var delegate: WebSocketStreamDelegate? diff --git a/Sources/ApolloWebSocket/WebSocketTask.swift b/Sources/ApolloWebSocket/WebSocketTask.swift index 558d6549ae..0420c1c199 100644 --- a/Sources/ApolloWebSocket/WebSocketTask.swift +++ b/Sources/ApolloWebSocket/WebSocketTask.swift @@ -2,7 +2,6 @@ import Apollo #endif import Foundation -import Starscream /// A task to wrap sending/canceling operations over a websocket. final class WebSocketTask: Cancellable { diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 766643a159..10fabf5ae6 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -327,7 +327,7 @@ public class WebSocketTransport { /// Disconnects the websocket while setting the auto-reconnect value to false, /// allowing purposeful disconnects that do not dump existing subscriptions. - /// NOTE: You will receive an error on the subscription (should be a `Starscream.WSError` with code 1000) when the socket disconnects. + /// NOTE: You will receive an error on the subscription (should be a `WebSocket.WSError` with code 1000) when the socket disconnects. /// ALSO NOTE: To reconnect after calling this, you will need to call `resumeWebSocketConnection`. public func pauseWebSocketConnection() { self.reconnect.mutate { $0 = false } diff --git a/SwiftScripts/Package.resolved b/SwiftScripts/Package.resolved index bba40c14e3..1a0909f0c8 100644 --- a/SwiftScripts/Package.resolved +++ b/SwiftScripts/Package.resolved @@ -82,15 +82,6 @@ "version": "0.12.2" } }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/apollographql/Starscream", - "state": { - "branch": null, - "revision": "8cf77babe5901693396436f4f418a6db0f328b78", - "version": "3.1.2" - } - }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", From 4edf4c76803a6620d4b9190a061f387b6f92cb33 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 13 Aug 2021 12:41:20 -0700 Subject: [PATCH 10/14] Fix websocket protocol configuration --- Sources/ApolloWebSocket/WebSocket.swift | 18 +++++++++--------- .../StarWarsSubscriptionTests.swift | 5 ++--- .../StarWarsWebSocketTests.swift | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 4dbd69f494..508b641f4c 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -75,6 +75,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock static let headerWSConnectionName = "Connection" static let headerWSConnectionValue = "Upgrade" static let headerWSProtocolName = "Sec-WebSocket-Protocol" + static let headerWSProtocolValue = "graphql-ws" static let headerWSVersionName = "Sec-WebSocket-Version" static let headerWSVersionValue = "13" static let headerWSExtensionName = "Sec-WebSocket-Extensions" @@ -183,7 +184,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock } /// Used for setting protocols. - public init(request: URLRequest, protocols: [String]? = nil) { + public init(request: URLRequest) { self.request = request self.stream = FoundationStream() if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil { @@ -195,22 +196,21 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock } self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } - if let protocols = protocols, !protocols.isEmpty { - self.request.setValue(protocols.joined(separator: ","), - forHTTPHeaderField: Constants.headerWSProtocolName) - } + + self.request.setValue(Constants.headerWSProtocolValue, + forHTTPHeaderField: Constants.headerWSProtocolName) writeQueue.maxConcurrentOperationCount = 1 } - public convenience init(url: URL, protocols: [String]? = nil) { + public convenience init(url: URL) { var request = URLRequest(url: url) request.timeoutInterval = 5 - self.init(request: request, protocols: protocols) + self.init(request: request) } // Used for specifically setting the QOS for the write queue. - public convenience init(url: URL, writeQueueQOS: QualityOfService, protocols: [String]? = nil) { - self.init(url: url, protocols: protocols) + public convenience init(url: URL, writeQueueQOS: QualityOfService) { + self.init(url: url) writeQueue.qualityOfService = writeQueueQOS } diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 8ce959b33d..973484c6a4 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -3,7 +3,6 @@ import Apollo import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI -import Starscream class StarWarsSubscriptionTests: XCTestCase { var concurrentQueue: DispatchQueue! @@ -22,7 +21,7 @@ class StarWarsSubscriptionTests: XCTestCase { connectionStartedExpectation = self.expectation(description: "Web socket connected") webSocketTransport = WebSocketTransport( - websocket: DefaultWebSocket( + websocket: WebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url) ), store: ApolloStore() @@ -469,7 +468,7 @@ class StarWarsSubscriptionTests: XCTestCase { XCTAssertEqual(graphQLResult.data?.reviewAdded?.episode, .jedi) subscriptionExpectation.fulfill() case .failure(let error): - if let wsError = error as? Starscream.WSError { + if let wsError = error as? WebSocket.WSError { // This is an expected error on disconnection, ignore it. XCTAssertEqual(wsError.code, 1000) } else { diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index bdc0c97fc9..c72de86616 100755 --- a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift @@ -22,7 +22,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheDependentTesting { let store = ApolloStore(cache: cache) let networkTransport = WebSocketTransport( - websocket: DefaultWebSocket( + websocket: WebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url) ), store: store From 1ba577302545d85887e4adc473d62c18df0ed890 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 13 Aug 2021 12:49:16 -0700 Subject: [PATCH 11/14] Clean up integration tests --- .../StarWarsSubscriptionTests.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 973484c6a4..54842108a9 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -480,12 +480,10 @@ class StarWarsSubscriptionTests: XCTestCase { self.waitForSubscriptionsToStart() sendReview() - - // TODO: Uncomment this expectation once https://github.com/daltoniam/Starscream/issues/869 is addressed - // and we're actually getting a notification that the socket has disconnected -// self.disconnectedExpectation = self.expectation(description: "Web socket disconnected") + + self.disconnectedExpectation = self.expectation(description: "Web socket disconnected") webSocketTransport.pauseWebSocketConnection() -// self.wait(for: [self.disconnectedExpectation!], timeout: 10) + self.wait(for: [self.disconnectedExpectation!], timeout: 10) // This should not go through since the socket is paused sendReview() From f9ea3d674ae4bec70dfe0619d4f7f3992a8b70a2 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Mon, 16 Aug 2021 11:54:24 -0700 Subject: [PATCH 12/14] Doc changes from code review requests --- Sources/ApolloWebSocket/Compression.swift | 2 +- Sources/ApolloWebSocket/SSLClientCertificate.swift | 2 +- Sources/ApolloWebSocket/SSLSecurity.swift | 2 +- Sources/ApolloWebSocket/WebSocket.swift | 2 +- Sources/ApolloWebSocket/WebSocketStream.swift | 2 +- Sources/ApolloWebSocket/WebSocketTransport.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Compression.swift index ca3f330dc5..197e5e9cc9 100644 --- a/Sources/ApolloWebSocket/Compression.swift +++ b/Sources/ApolloWebSocket/Compression.swift @@ -3,7 +3,7 @@ // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 // // This is a derived work derived from -// Starscream(https://github.com/daltoniam/Starscream) +// Starscream (https://github.com/daltoniam/Starscream) // // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/SSLClientCertificate.swift b/Sources/ApolloWebSocket/SSLClientCertificate.swift index 7807ee310b..c70e65d5fa 100644 --- a/Sources/ApolloWebSocket/SSLClientCertificate.swift +++ b/Sources/ApolloWebSocket/SSLClientCertificate.swift @@ -3,7 +3,7 @@ // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 // // This is a derived work derived from -// Starscream(https://github.com/daltoniam/Starscream) +// Starscream (https://github.com/daltoniam/Starscream) // // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/SSLSecurity.swift b/Sources/ApolloWebSocket/SSLSecurity.swift index 2a386956c9..a618931f45 100644 --- a/Sources/ApolloWebSocket/SSLSecurity.swift +++ b/Sources/ApolloWebSocket/SSLSecurity.swift @@ -3,7 +3,7 @@ // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 // // This is a derived work derived from -// Starscream(https://github.com/daltoniam/Starscream) +// Starscream (https://github.com/daltoniam/Starscream) // // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/WebSocket.swift index 508b641f4c..c4c1bb3b6d 100644 --- a/Sources/ApolloWebSocket/WebSocket.swift +++ b/Sources/ApolloWebSocket/WebSocket.swift @@ -3,7 +3,7 @@ // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 // // This is a derived work derived from -// Starscream(https://github.com/daltoniam/Starscream) +// Starscream (https://github.com/daltoniam/Starscream) // // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/WebSocketStream.swift b/Sources/ApolloWebSocket/WebSocketStream.swift index 377f0ffe2b..fa60ef4b60 100644 --- a/Sources/ApolloWebSocket/WebSocketStream.swift +++ b/Sources/ApolloWebSocket/WebSocketStream.swift @@ -3,7 +3,7 @@ // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 // // This is a derived work derived from -// Starscream(https://github.com/daltoniam/Starscream) +// Starscream (https://github.com/daltoniam/Starscream) // // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 10fabf5ae6..7f7d643dc1 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -46,7 +46,7 @@ public class WebSocketTransport { } var socketConnectionState = Atomic(.disconnected) - /// Indicates if the websocket connection has been acknowledge by the server. + /// Indicates if the websocket connection has been acknowledged by the server. private var acked = false private var queue: [Int: String] = [:] From e6f32eb3e53bcbe3ce858e173a1314b2bca30592 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Aug 2021 13:55:37 -0700 Subject: [PATCH 13/14] Move Starscream files into subfolder --- Apollo.xcodeproj/project.pbxproj | 18 +++++++++++++----- .../{ => Starscream}/Compression.swift | 0 .../SSLClientCertificate.swift | 0 .../{ => Starscream}/SSLSecurity.swift | 0 .../{ => Starscream}/WebSocket.swift | 0 .../{ => Starscream}/WebSocketStream.swift | 0 6 files changed, 13 insertions(+), 5 deletions(-) rename Sources/ApolloWebSocket/{ => Starscream}/Compression.swift (100%) rename Sources/ApolloWebSocket/{ => Starscream}/SSLClientCertificate.swift (100%) rename Sources/ApolloWebSocket/{ => Starscream}/SSLSecurity.swift (100%) rename Sources/ApolloWebSocket/{ => Starscream}/WebSocket.swift (100%) rename Sources/ApolloWebSocket/{ => Starscream}/WebSocketStream.swift (100%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 54877e025a..ca803c3dc7 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -1175,18 +1175,14 @@ 9B7BDA9323FDE94C00ACD198 /* ApolloWebSocket */ = { isa = PBXGroup; children = ( + E676C11F26CB05F90091215A /* Starscream */, 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */, - DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */, - DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */, 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */, 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */, 9B7BDA9423FDE94C00ACD198 /* WebSocketError.swift */, 9B7BDA9523FDE94C00ACD198 /* WebSocketTask.swift */, 9B7BDA9923FDE94C00ACD198 /* WebSocketTransport.swift */, 9B7BDA9A23FDE94C00ACD198 /* Info.plist */, - DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */, - DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */, - DE181A3126C5C401000C0B9C /* Compression.swift */, ); name = ApolloWebSocket; path = Sources/ApolloWebSocket; @@ -1736,6 +1732,18 @@ path = WebSocket; sourceTree = ""; }; + E676C11F26CB05F90091215A /* Starscream */ = { + isa = PBXGroup; + children = ( + DE181A3126C5C401000C0B9C /* Compression.swift */, + DE181A2B26C5C0CB000C0B9C /* WebSocket.swift */, + DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */, + DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */, + DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */, + ); + path = Starscream; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ diff --git a/Sources/ApolloWebSocket/Compression.swift b/Sources/ApolloWebSocket/Starscream/Compression.swift similarity index 100% rename from Sources/ApolloWebSocket/Compression.swift rename to Sources/ApolloWebSocket/Starscream/Compression.swift diff --git a/Sources/ApolloWebSocket/SSLClientCertificate.swift b/Sources/ApolloWebSocket/Starscream/SSLClientCertificate.swift similarity index 100% rename from Sources/ApolloWebSocket/SSLClientCertificate.swift rename to Sources/ApolloWebSocket/Starscream/SSLClientCertificate.swift diff --git a/Sources/ApolloWebSocket/SSLSecurity.swift b/Sources/ApolloWebSocket/Starscream/SSLSecurity.swift similarity index 100% rename from Sources/ApolloWebSocket/SSLSecurity.swift rename to Sources/ApolloWebSocket/Starscream/SSLSecurity.swift diff --git a/Sources/ApolloWebSocket/WebSocket.swift b/Sources/ApolloWebSocket/Starscream/WebSocket.swift similarity index 100% rename from Sources/ApolloWebSocket/WebSocket.swift rename to Sources/ApolloWebSocket/Starscream/WebSocket.swift diff --git a/Sources/ApolloWebSocket/WebSocketStream.swift b/Sources/ApolloWebSocket/Starscream/WebSocketStream.swift similarity index 100% rename from Sources/ApolloWebSocket/WebSocketStream.swift rename to Sources/ApolloWebSocket/Starscream/WebSocketStream.swift From 6ebf72a33ce534f65779114e7776c8b2454f223a Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Aug 2021 14:37:24 -0700 Subject: [PATCH 14/14] Rename ApolloWebSocket default implementation subfolder --- Apollo.xcodeproj/project.pbxproj | 6 +++--- .../{Starscream => DefaultImplementation}/Compression.swift | 0 .../SSLClientCertificate.swift | 0 .../{Starscream => DefaultImplementation}/SSLSecurity.swift | 0 .../{Starscream => DefaultImplementation}/WebSocket.swift | 0 .../WebSocketStream.swift | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename Sources/ApolloWebSocket/{Starscream => DefaultImplementation}/Compression.swift (100%) rename Sources/ApolloWebSocket/{Starscream => DefaultImplementation}/SSLClientCertificate.swift (100%) rename Sources/ApolloWebSocket/{Starscream => DefaultImplementation}/SSLSecurity.swift (100%) rename Sources/ApolloWebSocket/{Starscream => DefaultImplementation}/WebSocket.swift (100%) rename Sources/ApolloWebSocket/{Starscream => DefaultImplementation}/WebSocketStream.swift (100%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index ca803c3dc7..eaec0484f4 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -1175,7 +1175,7 @@ 9B7BDA9323FDE94C00ACD198 /* ApolloWebSocket */ = { isa = PBXGroup; children = ( - E676C11F26CB05F90091215A /* Starscream */, + E676C11F26CB05F90091215A /* DefaultImplementation */, 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */, 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */, 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */, @@ -1732,7 +1732,7 @@ path = WebSocket; sourceTree = ""; }; - E676C11F26CB05F90091215A /* Starscream */ = { + E676C11F26CB05F90091215A /* DefaultImplementation */ = { isa = PBXGroup; children = ( DE181A3126C5C401000C0B9C /* Compression.swift */, @@ -1741,7 +1741,7 @@ DE181A2D26C5C299000C0B9C /* SSLClientCertificate.swift */, DE181A2F26C5C38E000C0B9C /* SSLSecurity.swift */, ); - path = Starscream; + path = DefaultImplementation; sourceTree = ""; }; /* End PBXGroup section */ diff --git a/Sources/ApolloWebSocket/Starscream/Compression.swift b/Sources/ApolloWebSocket/DefaultImplementation/Compression.swift similarity index 100% rename from Sources/ApolloWebSocket/Starscream/Compression.swift rename to Sources/ApolloWebSocket/DefaultImplementation/Compression.swift diff --git a/Sources/ApolloWebSocket/Starscream/SSLClientCertificate.swift b/Sources/ApolloWebSocket/DefaultImplementation/SSLClientCertificate.swift similarity index 100% rename from Sources/ApolloWebSocket/Starscream/SSLClientCertificate.swift rename to Sources/ApolloWebSocket/DefaultImplementation/SSLClientCertificate.swift diff --git a/Sources/ApolloWebSocket/Starscream/SSLSecurity.swift b/Sources/ApolloWebSocket/DefaultImplementation/SSLSecurity.swift similarity index 100% rename from Sources/ApolloWebSocket/Starscream/SSLSecurity.swift rename to Sources/ApolloWebSocket/DefaultImplementation/SSLSecurity.swift diff --git a/Sources/ApolloWebSocket/Starscream/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift similarity index 100% rename from Sources/ApolloWebSocket/Starscream/WebSocket.swift rename to Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift diff --git a/Sources/ApolloWebSocket/Starscream/WebSocketStream.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocketStream.swift similarity index 100% rename from Sources/ApolloWebSocket/Starscream/WebSocketStream.swift rename to Sources/ApolloWebSocket/DefaultImplementation/WebSocketStream.swift