Skip to content

Commit d6e8f68

Browse files
gh-action-runnergh-action-runner
authored andcommitted
Squashed 'apollo-ios/' changes from ee597100..b6c8087e
b6c8087e fix: WebSocket data race crash (#578) git-subtree-dir: apollo-ios git-subtree-split: b6c8087ed34bd43afb327fa5a8ce72a01b846957
1 parent 2cee838 commit d6e8f68

File tree

2 files changed

+126
-116
lines changed

2 files changed

+126
-116
lines changed

Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
207207
private var connected = false
208208
private var isConnecting = false
209209
private let mutex = NSLock()
210+
private let serialQueue = DispatchQueue(label: "com.apollographql.WebSocket.serial", qos: .background)
210211
private var compressionState = CompressionState()
211212
private var writeQueue = OperationQueue()
212213
private var readStack = [WSResponse]()
@@ -278,10 +279,12 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
278279
Connect to the WebSocket server on a background thread.
279280
*/
280281
public func connect() {
281-
guard !isConnecting else { return }
282-
didDisconnect = false
283-
isConnecting = true
284-
createHTTPRequest()
282+
serialQueue.sync {
283+
guard !self.isConnecting else { return }
284+
self.didDisconnect = false
285+
self.isConnecting = true
286+
self.createHTTPRequest()
287+
}
285288
}
286289

287290
/**
@@ -1106,19 +1109,21 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
11061109
Used to preform the disconnect delegate
11071110
*/
11081111
private func doDisconnect(_ error: (any Error)?) {
1109-
guard !didDisconnect else { return }
1110-
didDisconnect = true
1111-
isConnecting = false
1112-
mutex.lock()
1113-
connected = false
1114-
mutex.unlock()
1115-
guard canDispatch else {return}
1116-
callbackQueue.async { [weak self] in
1117-
guard let self = self else { return }
1118-
self.onDisconnect?(error)
1119-
self.delegate?.websocketDidDisconnect(socket: self, error: error)
1120-
let userInfo = error.map{ [Constants.WebsocketDisconnectionErrorKeyName: $0] }
1121-
NotificationCenter.default.post(name: NSNotification.Name(Constants.Notifications.WebsocketDidDisconnect), object: self, userInfo: userInfo)
1112+
serialQueue.sync {
1113+
guard !self.didDisconnect else { return }
1114+
self.didDisconnect = true
1115+
self.isConnecting = false
1116+
self.mutex.lock()
1117+
self.connected = false
1118+
self.mutex.unlock()
1119+
guard self.canDispatch else {return}
1120+
self.callbackQueue.async { [weak self] in
1121+
guard let self = self else { return }
1122+
self.onDisconnect?(error)
1123+
self.delegate?.websocketDidDisconnect(socket: self, error: error)
1124+
let userInfo = error.map{ [Constants.WebsocketDisconnectionErrorKeyName: $0] }
1125+
NotificationCenter.default.post(name: NSNotification.Name(Constants.Notifications.WebsocketDidDisconnect), object: self, userInfo: userInfo)
1126+
}
11221127
}
11231128
}
11241129

Sources/ApolloWebSocket/DefaultImplementation/WebSocketStream.swift

Lines changed: 104 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -46,115 +46,118 @@ class FoundationStream : NSObject, WebSocketStream, StreamDelegate, SOCKSProxyab
4646
private let workQueue = DispatchQueue(label: "com.apollographql.websocket", attributes: [])
4747
private var inputStream: InputStream?
4848
private var outputStream: OutputStream?
49+
private let serialQueue = DispatchQueue(label: "com.apollographql.WebSocketStream.serial", qos: .background)
4950
weak var delegate: (any WebSocketStreamDelegate)?
5051
let BUFFER_MAX = 4096
5152

5253
var enableSOCKSProxy = false
5354

5455
func connect(url: URL, port: Int, timeout: TimeInterval, ssl: SSLSettings, completion: @escaping (((any Error)?) -> Void)) {
55-
var readStream: Unmanaged<CFReadStream>?
56-
var writeStream: Unmanaged<CFWriteStream>?
57-
let h = url.host! as NSString
58-
CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream)
59-
inputStream = readStream!.takeRetainedValue()
60-
outputStream = writeStream!.takeRetainedValue()
61-
62-
#if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work
63-
#else
64-
if enableSOCKSProxy {
65-
let proxyDict = CFNetworkCopySystemProxySettings()
66-
let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue())
67-
let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
68-
CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig)
69-
CFReadStreamSetProperty(inputStream, propertyKey, socksConfig)
70-
}
71-
#endif
72-
73-
guard let inStream = inputStream, let outStream = outputStream else { return }
74-
inStream.delegate = self
75-
outStream.delegate = self
76-
if ssl.useSSL {
77-
inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey)
78-
outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey)
56+
serialQueue.sync {
57+
var readStream: Unmanaged<CFReadStream>?
58+
var writeStream: Unmanaged<CFWriteStream>?
59+
let h = url.host! as NSString
60+
CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream)
61+
inputStream = readStream!.takeRetainedValue()
62+
outputStream = writeStream!.takeRetainedValue()
63+
7964
#if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work
8065
#else
81-
var settings = [NSObject: NSObject]()
82-
if ssl.disableCertValidation {
83-
settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: false)
84-
}
85-
if ssl.overrideTrustHostname {
86-
if let hostname = ssl.desiredTrustHostname {
87-
settings[kCFStreamSSLPeerName] = hostname as NSString
88-
} else {
89-
settings[kCFStreamSSLPeerName] = kCFNull
90-
}
91-
}
92-
if let sslClientCertificate = ssl.sslClientCertificate {
93-
settings[kCFStreamSSLCertificates] = sslClientCertificate.streamSSLCertificates
66+
if enableSOCKSProxy {
67+
let proxyDict = CFNetworkCopySystemProxySettings()
68+
let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue())
69+
let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
70+
CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig)
71+
CFReadStreamSetProperty(inputStream, propertyKey, socksConfig)
9472
}
95-
96-
inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
97-
outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
9873
#endif
9974

100-
#if os(Linux)
101-
#else
102-
if let cipherSuites = ssl.cipherSuites {
75+
guard let inStream = inputStream, let outStream = outputStream else { return }
76+
inStream.delegate = self
77+
outStream.delegate = self
78+
if ssl.useSSL {
79+
inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey)
80+
outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey)
10381
#if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work
10482
#else
105-
if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?,
106-
let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? {
107-
let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count)
108-
let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count)
109-
if resIn != errSecSuccess {
110-
completion(WebSocket.WSError(
111-
type: .invalidSSLError,
112-
message: "Error setting ingoing cypher suites",
113-
code: Int(resIn)))
83+
var settings = [NSObject: NSObject]()
84+
if ssl.disableCertValidation {
85+
settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: false)
86+
}
87+
if ssl.overrideTrustHostname {
88+
if let hostname = ssl.desiredTrustHostname {
89+
settings[kCFStreamSSLPeerName] = hostname as NSString
90+
} else {
91+
settings[kCFStreamSSLPeerName] = kCFNull
11492
}
115-
if resOut != errSecSuccess {
116-
completion(WebSocket.WSError(
117-
type: .invalidSSLError,
118-
message: "Error setting outgoing cypher suites",
119-
code: Int(resOut)))
93+
}
94+
if let sslClientCertificate = ssl.sslClientCertificate {
95+
settings[kCFStreamSSLCertificates] = sslClientCertificate.streamSSLCertificates
96+
}
97+
98+
inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
99+
outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
100+
#endif
101+
102+
#if os(Linux)
103+
#else
104+
if let cipherSuites = ssl.cipherSuites {
105+
#if os(watchOS) //watchOS us unfortunately is missing the kCFStream properties to make this work
106+
#else
107+
if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?,
108+
let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? {
109+
let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count)
110+
let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count)
111+
if resIn != errSecSuccess {
112+
completion(WebSocket.WSError(
113+
type: .invalidSSLError,
114+
message: "Error setting ingoing cypher suites",
115+
code: Int(resIn)))
116+
}
117+
if resOut != errSecSuccess {
118+
completion(WebSocket.WSError(
119+
type: .invalidSSLError,
120+
message: "Error setting outgoing cypher suites",
121+
code: Int(resOut)))
122+
}
120123
}
124+
#endif
121125
}
122126
#endif
123127
}
124-
#endif
125-
}
126128

127-
CFReadStreamSetDispatchQueue(inStream, workQueue)
128-
CFWriteStreamSetDispatchQueue(outStream, workQueue)
129-
inStream.open()
130-
outStream.open()
131-
132-
var out = timeout// wait X seconds before giving up
133-
workQueue.async { [weak self] in
134-
while !outStream.hasSpaceAvailable {
135-
usleep(100) // wait until the socket is ready
136-
out -= 100
137-
if out < 0 {
138-
completion(
139-
WebSocket.WSError(
140-
type: .writeTimeoutError,
141-
message: "Timed out waiting for the socket to be ready for a write",
142-
code: 0))
143-
return
144-
145-
} else if let error = outStream.streamError {
146-
completion(error)
147-
return // disconnectStream will be called.
148-
149-
} else if self == nil {
150-
completion(WebSocket.WSError(
151-
type: .closeError,
152-
message: "socket object has been dereferenced",
153-
code: 0))
154-
return
129+
CFReadStreamSetDispatchQueue(inStream, workQueue)
130+
CFWriteStreamSetDispatchQueue(outStream, workQueue)
131+
inStream.open()
132+
outStream.open()
133+
134+
var out = timeout// wait X seconds before giving up
135+
workQueue.async { [weak self] in
136+
while !outStream.hasSpaceAvailable {
137+
usleep(100) // wait until the socket is ready
138+
out -= 100
139+
if out < 0 {
140+
completion(
141+
WebSocket.WSError(
142+
type: .writeTimeoutError,
143+
message: "Timed out waiting for the socket to be ready for a write",
144+
code: 0))
145+
return
146+
147+
} else if let error = outStream.streamError {
148+
completion(error)
149+
return // disconnectStream will be called.
150+
151+
} else if self == nil {
152+
completion(WebSocket.WSError(
153+
type: .closeError,
154+
message: "socket object has been dereferenced",
155+
code: 0))
156+
return
157+
}
155158
}
159+
completion(nil) //success!
156160
}
157-
completion(nil) //success!
158161
}
159162
}
160163

@@ -176,18 +179,20 @@ class FoundationStream : NSObject, WebSocketStream, StreamDelegate, SOCKSProxyab
176179
}
177180

178181
func cleanup() {
179-
if let stream = inputStream {
180-
stream.delegate = nil
181-
CFReadStreamSetDispatchQueue(stream, nil)
182-
stream.close()
183-
}
184-
if let stream = outputStream {
185-
stream.delegate = nil
186-
CFWriteStreamSetDispatchQueue(stream, nil)
187-
stream.close()
182+
serialQueue.sync {
183+
if let stream = inputStream {
184+
stream.delegate = nil
185+
CFReadStreamSetDispatchQueue(stream, nil)
186+
stream.close()
187+
}
188+
if let stream = outputStream {
189+
stream.delegate = nil
190+
CFWriteStreamSetDispatchQueue(stream, nil)
191+
stream.close()
192+
}
193+
outputStream = nil
194+
inputStream = nil
188195
}
189-
outputStream = nil
190-
inputStream = nil
191196
}
192197

193198
#if os(Linux) || os(watchOS)

0 commit comments

Comments
 (0)