* Add & use WebSocketError type Previously, there were separate errors thrown for Starscream and native web sockets. Now, they both throw `WebSocketError`s. Also, if there’s a "Retry-After" header, we’ll include it on errors. * Use coarser lock in SSKWebSocketNative This isn’t strictly required, but it makes the class easier to reason about, and it fixes a bug where writing to a web socket while that socket is being closed normally could allow the write error to be reported instead of the close error. * Fix web socket close/final message race condition There’s a comment in the code that describes the fix. In addition, this updates the native web socket to only post the first error that occurs. Both of the existing web socket implementations (OWSWebSocket & ProvisioningSocket) are written in such a way that these subsequent errors would be ignored, and it makes future code simpler if there’s at most one error that gets reported. * Make error required in web socket close delegate Before this change, the error was never nil when using the native socket, which is the implementation we use on iOS 13+. This change ensures it’s never nil on iOS 12, which improves consistency. In addition, this updates the iOS 12 implementation to skip reporting error if the user has explicitly requested the connection be closed, which also mimics the behavior on iOS 13+.
170 lines
5.3 KiB
Swift
170 lines
5.3 KiB
Swift
//
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
import SignalMessaging
|
|
import Starscream
|
|
|
|
@objc
|
|
public class WebSocketFactoryHybrid: NSObject, WebSocketFactory {
|
|
|
|
public var canBuildWebSocket: Bool { true }
|
|
|
|
public func buildSocket(request: URLRequest, callbackQueue: DispatchQueue) -> SSKWebSocket? {
|
|
if FeatureFlags.canUseNativeWebsocket, #available(iOS 13, *) {
|
|
return SSKWebSocketNative(request: request, callbackQueue: callbackQueue)
|
|
} else {
|
|
return SSKWebSocketStarScream(request: request, callbackQueue: callbackQueue)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
class SSKWebSocketStarScream: SSKWebSocket {
|
|
|
|
private static let idCounter = AtomicUInt()
|
|
public let id = SSKWebSocketStarScream.idCounter.increment()
|
|
|
|
private let socket: Starscream.WebSocket
|
|
|
|
public var callbackQueue: DispatchQueue { socket.callbackQueue }
|
|
|
|
fileprivate let httpResponseHeaders = AtomicOptional<[String: String]>(nil)
|
|
|
|
init(request: URLRequest, callbackQueue: DispatchQueue) {
|
|
let socket = WebSocket(request: request)
|
|
socket.callbackQueue = callbackQueue
|
|
socket.disableSSLCertValidation = true
|
|
socket.socketSecurityLevel = StreamSocketSecurityLevel.tlSv1_2
|
|
let security = SSLSecurity(certs: [SignalMessengerCertificate()], usePublicKeys: false)
|
|
security.validateEntireChain = false
|
|
socket.security = security
|
|
|
|
// TODO cipher suite selection
|
|
// socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
|
|
|
|
self.socket = socket
|
|
|
|
socket.delegate = self
|
|
socket.onHttpResponseHeaders = { [weak self] httpHeaders in
|
|
self?.httpResponseHeaders.set(httpHeaders)
|
|
}
|
|
}
|
|
|
|
// MARK: - SSKWebSocket
|
|
|
|
weak var delegate: SSKWebSocketDelegate?
|
|
|
|
private let hasEverConnected = AtomicBool(false)
|
|
private let shouldReportError = AtomicBool(true)
|
|
|
|
// This method is thread-safe.
|
|
var state: SSKWebSocketState {
|
|
if socket.isConnected {
|
|
return .open
|
|
}
|
|
|
|
if hasEverConnected.get() {
|
|
return .disconnected
|
|
}
|
|
|
|
return .connecting
|
|
}
|
|
|
|
func connect() {
|
|
socket.connect()
|
|
}
|
|
|
|
func disconnect() {
|
|
shouldReportError.set(false)
|
|
socket.disconnect()
|
|
}
|
|
|
|
func write(data: Data) {
|
|
socket.write(data: data)
|
|
}
|
|
|
|
func writePing() {
|
|
socket.write(ping: Data())
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SSKWebSocketStarScream: WebSocketDelegate {
|
|
func websocketDidConnect(socket: WebSocketClient) {
|
|
assertOnQueue(callbackQueue)
|
|
hasEverConnected.set(true)
|
|
delegate?.websocketDidConnect(socket: self)
|
|
}
|
|
|
|
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
|
|
assertOnQueue(callbackQueue)
|
|
let resolvedError: Error
|
|
switch error {
|
|
case .some(let wsError as WSError) where wsError.type == .protocolError:
|
|
// Protocol errors include both normal closures & unexpected server behavior.
|
|
resolvedError = WebSocketError.closeError(
|
|
statusCode: wsError.code,
|
|
closeReason: wsError.message.data(using: .utf8)
|
|
)
|
|
|
|
case .some(let wsError as WSError) where wsError.type == .upgradeError:
|
|
// Upgrade errors occur in the HTTP layer during the web socket handshake.
|
|
let httpHeaders = OWSHttpHeaders(httpHeaders: httpResponseHeaders.get(), overwriteOnConflict: true)
|
|
resolvedError = WebSocketError.httpError(statusCode: wsError.code, retryAfter: httpHeaders.retryAfterDate)
|
|
|
|
case .some(let wsError as WSError):
|
|
resolvedError = wsError
|
|
|
|
case .some(let nsError as NSError):
|
|
// Assert that error is either a Starscream.WSError or an OS level networking error
|
|
assert(nsError.domain == NSPOSIXErrorDomain
|
|
|| nsError.domain == kCFErrorDomainCFNetwork as String
|
|
|| nsError.domain == NSOSStatusErrorDomain)
|
|
resolvedError = nsError
|
|
|
|
case .none:
|
|
// Based on how we use Starscream, we only expect a `nil` error in the case
|
|
// where the underlying TCP connection is closed without going through the
|
|
// normal web socket handshake. This should be reported as an error.
|
|
resolvedError = OWSGenericError("Unexpected end of stream.")
|
|
}
|
|
|
|
guard shouldReportError.tryToClearFlag() else {
|
|
return
|
|
}
|
|
|
|
delegate?.websocketDidDisconnectOrFail(socket: self, error: resolvedError)
|
|
}
|
|
|
|
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
|
|
assertOnQueue(callbackQueue)
|
|
owsFailDebug("We only expect binary frames.")
|
|
}
|
|
|
|
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
|
|
assertOnQueue(callbackQueue)
|
|
delegate?.websocket(self, didReceiveData: data)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func SignalMessengerCertificate() -> SSLCert {
|
|
let data = SSKSignalMessengerCertificateData()
|
|
return SSLCert(data: data)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private extension StreamSocketSecurityLevel {
|
|
static var tlSv1_2: StreamSocketSecurityLevel {
|
|
return StreamSocketSecurityLevel(rawValue: "kCFStreamSocketSecurityLevelTLSv1_2")
|
|
}
|
|
}
|