@cnhongwei/react-native-mqtt 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +169 -2
- package/README.zh-CN.md +100 -0
- package/ReactNativeMqtt.podspec +28 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
- package/ios/ReactNativeMqtt.h +15 -0
- package/ios/ReactNativeMqtt.mm +1184 -0
- package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
- package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
- package/ios/ReactNativeMqttSession.swift +420 -0
- package/lib/module/NativeReactNativeMqtt.js +5 -0
- package/lib/module/NativeReactNativeMqtt.js.map +1 -0
- package/lib/module/client.js +555 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/errors.js +20 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/helpers.js +116 -0
- package/lib/module/helpers.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validation.js +306 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
- package/lib/typescript/src/client.d.ts +3 -0
- package/lib/typescript/src/client.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/helpers.d.ts +5 -0
- package/lib/typescript/src/helpers.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +161 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +13 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +188 -4
- package/react-native.config.js +1 -0
- package/src/NativeReactNativeMqtt.ts +173 -0
- package/src/client.ts +859 -0
- package/src/errors.ts +51 -0
- package/src/helpers.ts +151 -0
- package/src/index.ts +30 -0
- package/src/types.ts +184 -0
- package/src/validation.ts +735 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import CocoaMQTT
|
|
2
|
+
import Foundation
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
private extension Array {
|
|
6
|
+
subscript(safe index: Int) -> Element? {
|
|
7
|
+
indices.contains(index) ? self[index] : nil
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private func mqttReasonError(_ message: String, reasonCode: Int, reasonString: String? = nil) -> NSError {
|
|
12
|
+
NSError(
|
|
13
|
+
domain: "ReactNativeMqtt",
|
|
14
|
+
code: reasonCode,
|
|
15
|
+
userInfo: [
|
|
16
|
+
NSLocalizedDescriptionKey: message,
|
|
17
|
+
"reasonCode": reasonCode,
|
|
18
|
+
"reasonString": reasonString ?? message,
|
|
19
|
+
]
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct ReactNativeMqttCocoaConnAck {
|
|
24
|
+
let sessionPresent: Bool
|
|
25
|
+
let reasonCode: UInt8
|
|
26
|
+
let reasonString: String?
|
|
27
|
+
let assignedClientIdentifier: String?
|
|
28
|
+
let userProperties: [CocoaMQTTUserProperty]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private func configureCocoaSocket(_ socket: CocoaMQTTSocketProtocol, serverName: String, caBase64: String?, p12Base64: String?, p12Password: String?, insecure: Bool) throws -> [SecCertificate] {
|
|
32
|
+
guard caBase64 != nil || p12Base64 != nil || insecure else { return [] }
|
|
33
|
+
var certificates = [SecCertificate]()
|
|
34
|
+
if let caBase64 {
|
|
35
|
+
guard let data = Data(base64Encoded: caBase64), let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
|
|
36
|
+
throw NSError(domain: "ReactNativeMqtt", code: 40, userInfo: [NSLocalizedDescriptionKey: "tls.caBase64 is not a valid certificate"])
|
|
37
|
+
}
|
|
38
|
+
certificates.append(certificate)
|
|
39
|
+
}
|
|
40
|
+
var identity: CocoaMQTTClientIdentity?
|
|
41
|
+
if let p12Base64 {
|
|
42
|
+
guard let data = Data(base64Encoded: p12Base64) else {
|
|
43
|
+
throw NSError(domain: "ReactNativeMqtt", code: 41, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 is not valid base64"])
|
|
44
|
+
}
|
|
45
|
+
var imported: CFArray?
|
|
46
|
+
let status = SecPKCS12Import(data as NSData, [kSecImportExportPassphrase as String: p12Password ?? ""] as NSDictionary, &imported)
|
|
47
|
+
guard status == errSecSuccess, let items = imported as? [[String: Any]], let rawIdentity = items.first?[kSecImportItemIdentity as String] else {
|
|
48
|
+
throw NSError(domain: "ReactNativeMqtt", code: 42, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 or tls.p12Password is invalid"])
|
|
49
|
+
}
|
|
50
|
+
guard CFGetTypeID(rawIdentity as CFTypeRef) == SecIdentityGetTypeID() else {
|
|
51
|
+
throw NSError(domain: "ReactNativeMqtt", code: 42, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 did not contain a client identity"])
|
|
52
|
+
}
|
|
53
|
+
// Bridge the checked Core Foundation object, not the Swift `Any`
|
|
54
|
+
// existential (which has a different representation on arm64).
|
|
55
|
+
let secIdentity = unsafeBitCast(rawIdentity as CFTypeRef, to: SecIdentity.self)
|
|
56
|
+
identity = CocoaMQTTClientIdentity(identity: secIdentity)
|
|
57
|
+
}
|
|
58
|
+
if let raw = socket as? CocoaMQTTSocket {
|
|
59
|
+
raw.tlsServerName = serverName
|
|
60
|
+
raw.trustedServerCertificates = certificates
|
|
61
|
+
raw.clientIdentity = identity
|
|
62
|
+
raw.usesSystemTrustStore = !insecure && certificates.isEmpty
|
|
63
|
+
raw.manuallyEvaluateTrust = insecure
|
|
64
|
+
} else if let webSocket = socket as? CocoaMQTTWebSocket {
|
|
65
|
+
webSocket.tlsServerName = serverName
|
|
66
|
+
webSocket.trustedServerCertificates = certificates
|
|
67
|
+
webSocket.clientIdentity = identity
|
|
68
|
+
webSocket.usesSystemTrustStore = !insecure && certificates.isEmpty
|
|
69
|
+
webSocket.manuallyEvaluateTrust = insecure
|
|
70
|
+
}
|
|
71
|
+
return certificates
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private func commonName(_ certificate: SecCertificate) -> String? {
|
|
75
|
+
var value: CFString?
|
|
76
|
+
guard SecCertificateCopyCommonName(certificate, &value) == errSecSuccess else { return nil }
|
|
77
|
+
return value as String?
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func hasSubjectAlternativeNames(_ certificate: SecCertificate) -> Bool {
|
|
81
|
+
let data = SecCertificateCopyData(certificate) as Data
|
|
82
|
+
let oid: [UInt8] = [0x06, 0x03, 0x55, 0x1d, 0x11]
|
|
83
|
+
return data.withUnsafeBytes { bytes in
|
|
84
|
+
guard let base = bytes.baseAddress else { return false }
|
|
85
|
+
guard data.count >= oid.count else { return false }
|
|
86
|
+
let values = base.assumingMemoryBound(to: UInt8.self)
|
|
87
|
+
return (0...(data.count - oid.count)).contains { index in
|
|
88
|
+
oid.enumerated().allSatisfy { values[index + $0.offset] == $0.element }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func evaluateConfiguredServerTrust(_ trust: SecTrust, serverName: String, certificates: [SecCertificate], allowInvalidHostnameForTesting: Bool = false) -> Bool {
|
|
94
|
+
let leaf = SecTrustGetCertificateAtIndex(trust, 0)
|
|
95
|
+
let cnFallback = allowInvalidHostnameForTesting && serverName.caseInsensitiveCompare("test.mosquitto.org") == .orderedSame && leaf.map { !hasSubjectAlternativeNames($0) && commonName($0)?.caseInsensitiveCompare(serverName) == .orderedSame } == true
|
|
96
|
+
guard SecTrustSetPolicies(trust, cnFallback ? SecPolicyCreateBasicX509() : SecPolicyCreateSSL(true, serverName as CFString)) == errSecSuccess else { return false }
|
|
97
|
+
if !certificates.isEmpty {
|
|
98
|
+
guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess,
|
|
99
|
+
SecTrustSetAnchorCertificatesOnly(trust, true) == errSecSuccess else { return false }
|
|
100
|
+
}
|
|
101
|
+
var error: CFError?
|
|
102
|
+
return SecTrustEvaluateWithError(trust, &error)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private protocol ReactNativeMqttCocoaTransporting: AnyObject {
|
|
106
|
+
func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)
|
|
107
|
+
func disconnect()
|
|
108
|
+
func disconnect(completion: @escaping (Error?) -> Void)
|
|
109
|
+
func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void)
|
|
110
|
+
func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void)
|
|
111
|
+
func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void)
|
|
112
|
+
func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void)
|
|
113
|
+
func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void)
|
|
114
|
+
func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// A single-owner MQTT 5 transport used by the native session.
|
|
118
|
+
/// CocoaMQTT's automatic reconnect is deliberately disabled; retry ownership
|
|
119
|
+
/// remains in the Session state machine.
|
|
120
|
+
final class ReactNativeMqttCocoaTransport: NSObject, CocoaMQTT5Delegate, ReactNativeMqttCocoaTransporting {
|
|
121
|
+
private let probe: ReactNativeMqttConnAckProbe
|
|
122
|
+
private let mqtt: CocoaMQTT5
|
|
123
|
+
private let callbackQueue = DispatchQueue(label: "com.cnhongwei.reactnativemqtt.cocoa-callback")
|
|
124
|
+
private var connectCompletion: ((Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)?
|
|
125
|
+
private var subscribeCompletion: ((Result<NSNumber, Error>) -> Void)?
|
|
126
|
+
private var subscribeBatchCompletion: ((Result<[NSNumber], Error>) -> Void)?
|
|
127
|
+
private var pendingSubscribeFilters: [String] = []
|
|
128
|
+
private var unsubscribeCompletion: ((Result<Void, Error>) -> Void)?
|
|
129
|
+
private var publishCompletions: [UInt16: (Result<Void, Error>) -> Void] = [:]
|
|
130
|
+
private var messageHandler: ((String, NSData, NSNumber, Bool, NSDictionary?) -> Void)?
|
|
131
|
+
private var disconnectedHandler: ((NSNumber?, String?) -> Void)?
|
|
132
|
+
private var serverDisconnectReasonCode: NSNumber?
|
|
133
|
+
private var remoteDisconnectNotified = false
|
|
134
|
+
private var disconnectCompletion: ((Error?) -> Void)?
|
|
135
|
+
private var suppressDisconnectHandler = false
|
|
136
|
+
private var configurationError: Error?
|
|
137
|
+
private let insecureTrust: Bool
|
|
138
|
+
private let allowInvalidHostnameForTesting: Bool
|
|
139
|
+
private let tlsServerName: String
|
|
140
|
+
private var configuredServerCertificates = [SecCertificate]()
|
|
141
|
+
|
|
142
|
+
init(clientId: String, host: String, port: UInt16, transport: String = "tcp", path: String = "/mqtt", username: String? = nil, password: String? = nil, caBase64: String? = nil, p12Base64: String? = nil, p12Password: String? = nil, insecure: Bool = false, allowInvalidHostnameForTesting: Bool = false, keepAlive: UInt16 = 60, cleanStart: Bool = true, sessionExpiryInterval: UInt32 = 0, receiveMaximum: UInt16 = UInt16.max, userProperties: [NSDictionary] = [], willTopic: String? = nil, willPayloadBase64: String? = nil, willQos: UInt8 = 0, willRetained: Bool = false) {
|
|
143
|
+
var socket: CocoaMQTTSocketProtocol = (transport == "ws" || transport == "wss") ? CocoaMQTTWebSocket(uri: path) : CocoaMQTTSocket()
|
|
144
|
+
socket.enableSSL = transport == "tls" || transport == "wss"
|
|
145
|
+
probe = ReactNativeMqttConnAckProbe(socket: socket)
|
|
146
|
+
mqtt = CocoaMQTT5(clientID: clientId, host: host, port: port, socket: probe)
|
|
147
|
+
insecureTrust = insecure
|
|
148
|
+
self.allowInvalidHostnameForTesting = allowInvalidHostnameForTesting
|
|
149
|
+
tlsServerName = host
|
|
150
|
+
super.init()
|
|
151
|
+
do { configuredServerCertificates = try configureCocoaSocket(socket, serverName: host, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure) } catch { configurationError = error }
|
|
152
|
+
mqtt.username = username
|
|
153
|
+
mqtt.password = password
|
|
154
|
+
mqtt.keepAlive = keepAlive
|
|
155
|
+
mqtt.cleanSession = cleanStart
|
|
156
|
+
let connectProperties = MqttConnectProperties()
|
|
157
|
+
connectProperties.sessionExpiryInterval = sessionExpiryInterval
|
|
158
|
+
connectProperties.receiveMaximum = receiveMaximum
|
|
159
|
+
connectProperties.userPropertyList = userProperties.compactMap { pair in
|
|
160
|
+
guard let key = pair["key"] as? String, let value = pair["value"] as? String else { return nil }
|
|
161
|
+
return CocoaMQTTUserProperty(key: key, value: value)
|
|
162
|
+
}
|
|
163
|
+
mqtt.connectProperties = connectProperties
|
|
164
|
+
if let willTopic, let willPayloadBase64, let willPayload = Data(base64Encoded: willPayloadBase64) {
|
|
165
|
+
mqtt.willMessage = CocoaMQTT5Message(topic: willTopic, payload: [UInt8](willPayload), qos: CocoaMQTTQoS(rawValue: willQos) ?? .qos0, retained: willRetained)
|
|
166
|
+
} else if willTopic != nil || willPayloadBase64 != nil {
|
|
167
|
+
configurationError = NSError(domain: "ReactNativeMqtt", code: 43, userInfo: [NSLocalizedDescriptionKey: "willPayloadBase64 is invalid"])
|
|
168
|
+
}
|
|
169
|
+
mqtt.autoReconnect = false
|
|
170
|
+
mqtt.delegate = self
|
|
171
|
+
mqtt.delegateQueue = callbackQueue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void) {
|
|
175
|
+
if let configurationError { completion(.failure(configurationError)); return }
|
|
176
|
+
remoteDisconnectNotified = false
|
|
177
|
+
connectCompletion = completion
|
|
178
|
+
guard mqtt.connect() else {
|
|
179
|
+
connectCompletion = nil
|
|
180
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 1, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected connect before socket I/O"])))
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func disconnect() {
|
|
186
|
+
suppressDisconnectHandler = true
|
|
187
|
+
mqtt.disconnect()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func disconnect(completion: @escaping (Error?) -> Void) {
|
|
191
|
+
suppressDisconnectHandler = true
|
|
192
|
+
disconnectCompletion = completion
|
|
193
|
+
mqtt.disconnect()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
|
|
197
|
+
disconnectedHandler = handler
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
|
|
201
|
+
messageHandler = handler
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void) {
|
|
205
|
+
subscribeBatch([(filter, qos, noLocal, retainAsPublished, retainHandling)]) { result in
|
|
206
|
+
switch result {
|
|
207
|
+
case .success(let granted): completion(.success(granted[0]))
|
|
208
|
+
case .failure(let error): completion(.failure(error))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void) {
|
|
214
|
+
guard !subscriptions.isEmpty else { completion(.success([])); return }
|
|
215
|
+
let parsed = subscriptions.map { item -> MqttSubscription? in
|
|
216
|
+
guard let qos = CocoaMQTTQoS(rawValue: item.qos), qos != .FAILURE,
|
|
217
|
+
let retainHandling = CocoaRetainHandlingOption(rawValue: item.retainHandling) else { return nil }
|
|
218
|
+
let subscription = MqttSubscription(topic: item.filter, qos: qos)
|
|
219
|
+
subscription.noLocal = item.noLocal
|
|
220
|
+
subscription.retainAsPublished = item.retainAsPublished
|
|
221
|
+
subscription.retainHandling = retainHandling
|
|
222
|
+
return subscription
|
|
223
|
+
}
|
|
224
|
+
guard parsed.allSatisfy({ $0 != nil }) else {
|
|
225
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 10, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription options"])))
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
pendingSubscribeFilters = subscriptions.map(\.filter)
|
|
229
|
+
subscribeBatchCompletion = completion
|
|
230
|
+
mqtt.subscribe(parsed.compactMap { $0 })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func completeSubscribeBatch(success: NSDictionary, failed: [String], reasonCodes: [UInt8] = []) {
|
|
234
|
+
guard let completion = subscribeBatchCompletion else { return }
|
|
235
|
+
let results = pendingSubscribeFilters.enumerated().map { index, filter -> NSNumber in
|
|
236
|
+
if let granted = success[filter] as? NSNumber { return granted }
|
|
237
|
+
if let reason = reasonCodes[safe: index] { return NSNumber(value: reason) }
|
|
238
|
+
// A missing or malformed per-filter ACK is a protocol failure; never
|
|
239
|
+
// turn an unaccounted entry into a successful subscription.
|
|
240
|
+
return NSNumber(value: 0x80)
|
|
241
|
+
}
|
|
242
|
+
subscribeBatchCompletion = nil
|
|
243
|
+
pendingSubscribeFilters = []
|
|
244
|
+
completion(.success(results))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func failSubscribeBatch(_ error: Error) {
|
|
248
|
+
subscribeBatchCompletion?(.failure(error))
|
|
249
|
+
subscribeBatchCompletion = nil
|
|
250
|
+
pendingSubscribeFilters = []
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
254
|
+
unsubscribeCompletion = completion
|
|
255
|
+
mqtt.unsubscribe(filter)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
259
|
+
guard let qos = CocoaMQTTQoS(rawValue: qos), qos != .FAILURE else {
|
|
260
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 11, userInfo: [NSLocalizedDescriptionKey: "Invalid publish QoS"])))
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
let message = CocoaMQTT5Message(topic: topic, payload: payload, qos: qos, retained: retained)
|
|
264
|
+
let publishProperties = MqttPublishProperties()
|
|
265
|
+
if let properties {
|
|
266
|
+
publishProperties.contentType = properties["contentType"] as? String
|
|
267
|
+
publishProperties.responseTopic = properties["responseTopic"] as? String
|
|
268
|
+
if let correlationData = properties["correlationData"] as? NSData {
|
|
269
|
+
publishProperties.correlationData = [UInt8](correlationData)
|
|
270
|
+
}
|
|
271
|
+
if let expiry = properties["messageExpiryIntervalSeconds"] as? NSNumber { publishProperties.messageExpiryInterval = expiry.uint32Value }
|
|
272
|
+
if let indicator = properties["payloadFormatIndicator"] as? NSNumber { publishProperties.payloadFormatIndicator = PayloadFormatIndicator(rawValue: indicator.uint8Value) }
|
|
273
|
+
if let userProperties = properties["userProperties"] as? [NSDictionary] {
|
|
274
|
+
publishProperties.userProperties = userProperties.compactMap { pair in
|
|
275
|
+
guard let key = pair["key"] as? String, let value = pair["value"] as? String else { return nil }
|
|
276
|
+
return CocoaMQTTUserProperty(key: key, value: value)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
let packetId = UInt16(truncatingIfNeeded: mqtt.publish(message, properties: publishProperties))
|
|
281
|
+
if qos == .qos0 {
|
|
282
|
+
completion(.success(()))
|
|
283
|
+
} else {
|
|
284
|
+
publishCompletions[packetId] = completion
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didConnectAck ack: CocoaMQTTCONNACKReasonCode, connAckData: MqttDecodeConnAck?) {
|
|
289
|
+
guard let sessionPresent = probe.sessionPresent else {
|
|
290
|
+
connectCompletion?(.failure(NSError(domain: "ReactNativeMqtt", code: 2, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT did not expose CONNACK sessionPresent"])))
|
|
291
|
+
connectCompletion = nil
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
let result = ReactNativeMqttCocoaConnAck(sessionPresent: sessionPresent, reasonCode: ack.rawValue, reasonString: connAckData?.reasonString, assignedClientIdentifier: connAckData?.assignedClientIdentifier, userProperties: connAckData?.userProperties ?? [])
|
|
295
|
+
connectCompletion?(.success(result))
|
|
296
|
+
connectCompletion = nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didPublishMessage message: CocoaMQTT5Message, id: UInt16) {}
|
|
300
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didPublishAck id: UInt16, pubAckData: MqttDecodePubAck?) {
|
|
301
|
+
guard pubAckData?.reasonCode == nil || pubAckData?.reasonCode == .success else {
|
|
302
|
+
let reason = pubAckData?.reasonCode?.rawValue ?? 0x80
|
|
303
|
+
publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBACK rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubAckData?.reasonString)))
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
publishCompletions.removeValue(forKey: id)?(.success(()))
|
|
307
|
+
}
|
|
308
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didPublishRec id: UInt16, pubRecData: MqttDecodePubRec?) {
|
|
309
|
+
guard pubRecData?.reasonCode == nil || pubRecData?.reasonCode == .success else {
|
|
310
|
+
let reason = pubRecData?.reasonCode?.rawValue ?? 0x80
|
|
311
|
+
publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBREC rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubRecData?.reasonString)))
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
// QoS 2 is complete only after PUBCOMP; PUBREC merely advances the exchange.
|
|
315
|
+
}
|
|
316
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didPublishComplete id: UInt16, pubCompData: MqttDecodePubComp?) {
|
|
317
|
+
guard pubCompData?.reasonCode == nil || pubCompData?.reasonCode == .success else {
|
|
318
|
+
let reason = pubCompData?.reasonCode?.rawValue ?? 0x80
|
|
319
|
+
publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBCOMP rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubCompData?.reasonString)))
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
publishCompletions.removeValue(forKey: id)?(.success(()))
|
|
323
|
+
}
|
|
324
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) {
|
|
325
|
+
var metadata: [String: Any] = [:]
|
|
326
|
+
if let publishData {
|
|
327
|
+
metadata["userProperties"] = publishData.userProperties.map { ["key": $0.key, "value": $0.value] }
|
|
328
|
+
if let contentType = publishData.contentType { metadata["contentType"] = contentType }
|
|
329
|
+
if let responseTopic = publishData.responseTopic { metadata["responseTopic"] = responseTopic }
|
|
330
|
+
if let correlationData = publishData.correlationData { metadata["correlationData"] = NSData(bytes: correlationData, length: correlationData.count) }
|
|
331
|
+
if let expiry = publishData.messageExpiryInterval { metadata["messageExpiryIntervalSeconds"] = NSNumber(value: expiry) }
|
|
332
|
+
if let indicator = publishData.payloadFormatIndicator { metadata["payloadFormatIndicator"] = NSNumber(value: indicator.rawValue) }
|
|
333
|
+
}
|
|
334
|
+
messageHandler?(message.topic, NSData(bytes: message.payload, length: message.payload.count), NSNumber(value: message.qos.rawValue), message.retained, metadata as NSDictionary)
|
|
335
|
+
}
|
|
336
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didSubscribeTopics success: NSDictionary, failed: [String], subAckData: MqttDecodeSubAck?) {
|
|
337
|
+
completeSubscribeBatch(success: success, failed: failed, reasonCodes: subAckData?.reasonCodes.map(\.rawValue) ?? [])
|
|
338
|
+
subscribeCompletion = nil
|
|
339
|
+
}
|
|
340
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didUnsubscribeTopics topics: [String], unsubAckData: MqttDecodeUnsubAck?) {
|
|
341
|
+
if let reasonCode = unsubAckData?.reasonCodes.first?.rawValue, reasonCode >= 0x80 {
|
|
342
|
+
unsubscribeCompletion?(.failure(mqttReasonError("UNSUBACK rejected", reasonCode: Int(reasonCode), reasonString: unsubAckData?.reasonString)))
|
|
343
|
+
} else {
|
|
344
|
+
unsubscribeCompletion?(.success(()))
|
|
345
|
+
}
|
|
346
|
+
unsubscribeCompletion = nil
|
|
347
|
+
}
|
|
348
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveDisconnectReasonCode reasonCode: CocoaMQTTDISCONNECTReasonCode) {
|
|
349
|
+
serverDisconnectReasonCode = NSNumber(value: reasonCode.rawValue)
|
|
350
|
+
// The MQTT DISCONNECT frame is already authoritative at this point. Do
|
|
351
|
+
// not wait for CocoaMQTT's later socket callback: on some transports that
|
|
352
|
+
// callback is delayed or omitted after a broker-initiated disconnect.
|
|
353
|
+
if !suppressDisconnectHandler && !remoteDisconnectNotified {
|
|
354
|
+
remoteDisconnectNotified = true
|
|
355
|
+
disconnectedHandler?(serverDisconnectReasonCode, nil)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveAuthReasonCode reasonCode: CocoaMQTTAUTHReasonCode) {}
|
|
359
|
+
func mqtt5(_ mqtt5: CocoaMQTT5, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
|
|
360
|
+
completionHandler(insecureTrust || (!configuredServerCertificates.isEmpty && evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)))
|
|
361
|
+
}
|
|
362
|
+
func mqtt5UrlSession(_ mqtt: CocoaMQTT5, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
363
|
+
if insecureTrust {
|
|
364
|
+
completionHandler(.useCredential, URLCredential(trust: trust))
|
|
365
|
+
} else if !configuredServerCertificates.isEmpty {
|
|
366
|
+
let accepted = evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)
|
|
367
|
+
completionHandler(accepted ? .useCredential : .cancelAuthenticationChallenge, accepted ? URLCredential(trust: trust) : nil)
|
|
368
|
+
} else {
|
|
369
|
+
completionHandler(.performDefaultHandling, nil)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
func mqtt5DidPing(_ mqtt5: CocoaMQTT5) {}
|
|
373
|
+
func mqtt5DidReceivePong(_ mqtt5: CocoaMQTT5) {}
|
|
374
|
+
func mqtt5DidDisconnect(_ mqtt5: CocoaMQTT5, withError err: Error?) {
|
|
375
|
+
if let completion = connectCompletion {
|
|
376
|
+
completion(.failure(err ?? NSError(domain: "ReactNativeMqtt", code: 3, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected before CONNACK"])))
|
|
377
|
+
connectCompletion = nil
|
|
378
|
+
}
|
|
379
|
+
let error = err ?? NSError(domain: "ReactNativeMqtt", code: 3, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected"])
|
|
380
|
+
failSubscribeBatch(error)
|
|
381
|
+
subscribeCompletion?(.failure(error))
|
|
382
|
+
unsubscribeCompletion?(.failure(error))
|
|
383
|
+
publishCompletions.values.forEach { $0(.failure(error)) }
|
|
384
|
+
subscribeCompletion = nil
|
|
385
|
+
unsubscribeCompletion = nil
|
|
386
|
+
publishCompletions.removeAll()
|
|
387
|
+
disconnectCompletion?(err)
|
|
388
|
+
disconnectCompletion = nil
|
|
389
|
+
// CocoaMQTT records a remote DISCONNECT before invoking this callback.
|
|
390
|
+
// Read the SDK-owned value here as the source of truth: the separate
|
|
391
|
+
// didReceiveDisconnectReasonCode delegate callback can be delivered out
|
|
392
|
+
// of order relative to the disconnect callback on some transports.
|
|
393
|
+
if let reason = mqtt5.lastDisconnectReason,
|
|
394
|
+
reason.source == .remote {
|
|
395
|
+
serverDisconnectReasonCode = NSNumber(value: reason.reasonCode.rawValue)
|
|
396
|
+
}
|
|
397
|
+
let suppress = suppressDisconnectHandler
|
|
398
|
+
let alreadyNotified = remoteDisconnectNotified
|
|
399
|
+
suppressDisconnectHandler = false
|
|
400
|
+
if !suppress && !alreadyNotified { disconnectedHandler?(serverDisconnectReasonCode, nil) }
|
|
401
|
+
remoteDisconnectNotified = false
|
|
402
|
+
serverDisconnectReasonCode = nil
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// MQTT 3.1.1 adapter. CocoaMQTT does not expose a session-present bit for
|
|
407
|
+
/// MQTT 3.1.1, so its CONNACK is reported with the protocol-defined false value.
|
|
408
|
+
final class ReactNativeMqttCocoa3Transport: NSObject, CocoaMQTTDelegate, ReactNativeMqttCocoaTransporting {
|
|
409
|
+
private let mqtt: CocoaMQTT
|
|
410
|
+
private let callbackQueue = DispatchQueue(label: "com.cnhongwei.reactnativemqtt.cocoa3-callback")
|
|
411
|
+
private var connectCompletion: ((Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)?
|
|
412
|
+
private var subscribeCompletion: ((Result<NSNumber, Error>) -> Void)?
|
|
413
|
+
private var subscribeBatchCompletion: ((Result<[NSNumber], Error>) -> Void)?
|
|
414
|
+
private var pendingSubscribeFilters: [String] = []
|
|
415
|
+
private var unsubscribeCompletion: ((Result<Void, Error>) -> Void)?
|
|
416
|
+
private var publishCompletions: [UInt16: (Result<Void, Error>) -> Void] = [:]
|
|
417
|
+
private var messageHandler: ((String, NSData, NSNumber, Bool, NSDictionary?) -> Void)?
|
|
418
|
+
private var disconnectedHandler: ((NSNumber?, String?) -> Void)?
|
|
419
|
+
private var disconnectCompletion: ((Error?) -> Void)?
|
|
420
|
+
private var suppressDisconnectHandler = false
|
|
421
|
+
private var configurationError: Error?
|
|
422
|
+
private let insecureTrust: Bool
|
|
423
|
+
private let allowInvalidHostnameForTesting: Bool
|
|
424
|
+
private let tlsServerName: String
|
|
425
|
+
private var configuredServerCertificates = [SecCertificate]()
|
|
426
|
+
|
|
427
|
+
init(clientId: String, host: String, port: UInt16, transport: String = "tcp", path: String = "/mqtt", username: String? = nil, password: String? = nil, caBase64: String? = nil, p12Base64: String? = nil, p12Password: String? = nil, insecure: Bool = false, allowInvalidHostnameForTesting: Bool = false, keepAlive: UInt16 = 60, cleanSession: Bool = true, willTopic: String? = nil, willPayloadBase64: String? = nil, willQos: UInt8 = 0, willRetained: Bool = false) {
|
|
428
|
+
var socket: CocoaMQTTSocketProtocol = (transport == "ws" || transport == "wss") ? CocoaMQTTWebSocket(uri: path) : CocoaMQTTSocket()
|
|
429
|
+
socket.enableSSL = transport == "tls" || transport == "wss"
|
|
430
|
+
mqtt = CocoaMQTT(clientID: clientId, host: host, port: port, socket: socket)
|
|
431
|
+
insecureTrust = insecure
|
|
432
|
+
self.allowInvalidHostnameForTesting = allowInvalidHostnameForTesting
|
|
433
|
+
tlsServerName = host
|
|
434
|
+
super.init()
|
|
435
|
+
do { configuredServerCertificates = try configureCocoaSocket(socket, serverName: host, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure) } catch { configurationError = error }
|
|
436
|
+
mqtt.username = username
|
|
437
|
+
mqtt.password = password
|
|
438
|
+
mqtt.keepAlive = keepAlive
|
|
439
|
+
mqtt.cleanSession = cleanSession
|
|
440
|
+
if let willTopic, let willPayloadBase64, let willPayload = Data(base64Encoded: willPayloadBase64) {
|
|
441
|
+
mqtt.willMessage = CocoaMQTTMessage(topic: willTopic, payload: [UInt8](willPayload), qos: CocoaMQTTQoS(rawValue: willQos) ?? .qos0, retained: willRetained)
|
|
442
|
+
} else if willTopic != nil || willPayloadBase64 != nil {
|
|
443
|
+
configurationError = NSError(domain: "ReactNativeMqtt", code: 43, userInfo: [NSLocalizedDescriptionKey: "willPayloadBase64 is invalid"])
|
|
444
|
+
}
|
|
445
|
+
mqtt.autoReconnect = false
|
|
446
|
+
mqtt.delegate = self
|
|
447
|
+
mqtt.delegateQueue = callbackQueue
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void) {
|
|
451
|
+
if let configurationError { completion(.failure(configurationError)); return }
|
|
452
|
+
connectCompletion = completion
|
|
453
|
+
guard mqtt.connect() else {
|
|
454
|
+
connectCompletion = nil
|
|
455
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 21, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected connect before socket I/O"])))
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func disconnect() { suppressDisconnectHandler = true; mqtt.disconnect() }
|
|
461
|
+
|
|
462
|
+
func disconnect(completion: @escaping (Error?) -> Void) {
|
|
463
|
+
suppressDisconnectHandler = true
|
|
464
|
+
disconnectCompletion = completion
|
|
465
|
+
mqtt.disconnect()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
|
|
469
|
+
disconnectedHandler = handler
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
|
|
473
|
+
messageHandler = handler
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void) {
|
|
477
|
+
subscribeBatch([(filter, qos, noLocal, retainAsPublished, retainHandling)]) { result in
|
|
478
|
+
switch result {
|
|
479
|
+
case .success(let granted): completion(.success(granted[0]))
|
|
480
|
+
case .failure(let error): completion(.failure(error))
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void) {
|
|
486
|
+
guard subscriptions.allSatisfy({ $0.noLocal == false && $0.retainAsPublished == false && $0.retainHandling == 0 }) else {
|
|
487
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 22, userInfo: [NSLocalizedDescriptionKey: "MQTT 3.1.1 does not support the requested subscription options"])))
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
let qosValues = subscriptions.map { CocoaMQTTQoS(rawValue: $0.qos) }
|
|
491
|
+
guard qosValues.allSatisfy({ $0 != nil && $0! != .FAILURE }) else {
|
|
492
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 22, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription QoS"])))
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
pendingSubscribeFilters = subscriptions.map(\.filter)
|
|
496
|
+
subscribeBatchCompletion = completion
|
|
497
|
+
mqtt.subscribe(zip(subscriptions, qosValues).map { ($0.0.filter, $0.1!) })
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private func completeSubscribeBatch(success: NSDictionary, failed: [String]) {
|
|
501
|
+
guard let completion = subscribeBatchCompletion else { return }
|
|
502
|
+
let results = pendingSubscribeFilters.map { filter -> NSNumber in
|
|
503
|
+
if let granted = success[filter] as? NSNumber { return granted }
|
|
504
|
+
return NSNumber(value: failed.contains(filter) ? 0x80 : 0x80)
|
|
505
|
+
}
|
|
506
|
+
subscribeBatchCompletion = nil
|
|
507
|
+
pendingSubscribeFilters = []
|
|
508
|
+
completion(.success(results))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func failSubscribeBatch(_ error: Error) {
|
|
512
|
+
subscribeBatchCompletion?(.failure(error))
|
|
513
|
+
subscribeBatchCompletion = nil
|
|
514
|
+
pendingSubscribeFilters = []
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
518
|
+
unsubscribeCompletion = completion
|
|
519
|
+
mqtt.unsubscribe(filter)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
523
|
+
guard let qos = CocoaMQTTQoS(rawValue: qos), qos != .FAILURE else {
|
|
524
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 23, userInfo: [NSLocalizedDescriptionKey: "Invalid publish QoS"])))
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
let packetId = mqtt.publish(CocoaMQTTMessage(topic: topic, payload: payload, qos: qos, retained: retained))
|
|
528
|
+
guard packetId >= 0 else {
|
|
529
|
+
completion(.failure(NSError(domain: "ReactNativeMqtt", code: 24, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected publish"])))
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
if qos == .qos0 { completion(.success(())) }
|
|
533
|
+
else { publishCompletions[UInt16(packetId)] = completion }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
|
|
537
|
+
let result = ReactNativeMqttCocoaConnAck(sessionPresent: false, reasonCode: ack.rawValue, reasonString: nil, assignedClientIdentifier: nil, userProperties: [])
|
|
538
|
+
connectCompletion?(.success(result))
|
|
539
|
+
connectCompletion = nil
|
|
540
|
+
}
|
|
541
|
+
func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
|
|
542
|
+
completionHandler(insecureTrust || (!configuredServerCertificates.isEmpty && evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)))
|
|
543
|
+
}
|
|
544
|
+
func mqttUrlSession(_ mqtt: CocoaMQTT, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
545
|
+
if insecureTrust {
|
|
546
|
+
completionHandler(.useCredential, URLCredential(trust: trust))
|
|
547
|
+
} else if !configuredServerCertificates.isEmpty {
|
|
548
|
+
let accepted = evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates)
|
|
549
|
+
completionHandler(accepted ? .useCredential : .cancelAuthenticationChallenge, accepted ? URLCredential(trust: trust) : nil)
|
|
550
|
+
} else {
|
|
551
|
+
completionHandler(.performDefaultHandling, nil)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {}
|
|
555
|
+
func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) { publishCompletions.removeValue(forKey: id)?(.success(())) }
|
|
556
|
+
func mqtt(_ mqtt: CocoaMQTT, didPublishComplete id: UInt16) { publishCompletions.removeValue(forKey: id)?(.success(())) }
|
|
557
|
+
func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
|
|
558
|
+
messageHandler?(message.topic, NSData(bytes: message.payload, length: message.payload.count), NSNumber(value: message.qos.rawValue), message.retained, nil)
|
|
559
|
+
}
|
|
560
|
+
func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
|
|
561
|
+
completeSubscribeBatch(success: success, failed: failed)
|
|
562
|
+
subscribeCompletion = nil
|
|
563
|
+
}
|
|
564
|
+
func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) { unsubscribeCompletion?(.success(())); unsubscribeCompletion = nil }
|
|
565
|
+
func mqttDidPing(_ mqtt: CocoaMQTT) {}
|
|
566
|
+
func mqttDidReceivePong(_ mqtt: CocoaMQTT) {}
|
|
567
|
+
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
|
|
568
|
+
let error = err ?? NSError(domain: "ReactNativeMqtt", code: 26, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected"])
|
|
569
|
+
connectCompletion?(.failure(error)); connectCompletion = nil
|
|
570
|
+
failSubscribeBatch(error)
|
|
571
|
+
subscribeCompletion?(.failure(error)); subscribeCompletion = nil
|
|
572
|
+
unsubscribeCompletion?(.failure(error)); unsubscribeCompletion = nil
|
|
573
|
+
publishCompletions.values.forEach { $0(.failure(error)) }
|
|
574
|
+
publishCompletions.removeAll()
|
|
575
|
+
disconnectCompletion?(err)
|
|
576
|
+
disconnectCompletion = nil
|
|
577
|
+
let suppress = suppressDisconnectHandler
|
|
578
|
+
suppressDisconnectHandler = false
|
|
579
|
+
if !suppress { disconnectedHandler?(nil, nil) }
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// Objective-C boundary for the TurboModule. CocoaMQTT types never cross this
|
|
584
|
+
/// boundary, so the ObjC++ session only receives Foundation values.
|
|
585
|
+
private final class ReactNativeMqttSessionTransportAdapter: ReactNativeMqttSessionTransport {
|
|
586
|
+
let underlying: ReactNativeMqttCocoaTransporting
|
|
587
|
+
private var sessionDisconnectedHandler: ((Error?) -> Void)?
|
|
588
|
+
var publicDisconnectedHandler: ((Error?) -> Void)?
|
|
589
|
+
|
|
590
|
+
init(underlying: ReactNativeMqttCocoaTransporting) {
|
|
591
|
+
self.underlying = underlying
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
func connect(completion: @escaping (Result<Any, Error>) -> Void) {
|
|
595
|
+
underlying.connect { result in
|
|
596
|
+
switch result {
|
|
597
|
+
case .success(let ack): completion(.success(ack))
|
|
598
|
+
case .failure(let error): completion(.failure(error))
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
func disconnect(completion: @escaping (Error?) -> Void) {
|
|
604
|
+
underlying.disconnect(completion: completion)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
func setDisconnectedHandler(_ handler: @escaping (Error?) -> Void) {
|
|
608
|
+
sessionDisconnectedHandler = handler
|
|
609
|
+
underlying.setDisconnectedHandler { [weak self] reasonCode, reasonString in
|
|
610
|
+
var userInfo: [String: Any] = [:]
|
|
611
|
+
if let reasonCode { userInfo["reasonCode"] = reasonCode }
|
|
612
|
+
if let reasonString { userInfo["reasonString"] = reasonString }
|
|
613
|
+
let error = userInfo.isEmpty ? nil : NSError(domain: "ReactNativeMqtt", code: 0, userInfo: userInfo)
|
|
614
|
+
self?.sessionDisconnectedHandler?(error)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@objc(ReactNativeMqttCocoaBridge)
|
|
620
|
+
public final class ReactNativeMqttCocoaBridge: NSObject {
|
|
621
|
+
private let transport: ReactNativeMqttCocoaTransporting
|
|
622
|
+
private let session: ReactNativeMqttSession
|
|
623
|
+
private let sessionTransport: ReactNativeMqttSessionTransportAdapter
|
|
624
|
+
private let operationLedger: ReactNativeMqttOperationLedger
|
|
625
|
+
private var sessionEventHandler: ((String, NSNumber, NSNumber, NSDictionary?) -> Void)?
|
|
626
|
+
|
|
627
|
+
@objc(initWithClientId:host:port:protocolVersion:transport:path:username:password:caBase64:p12Base64:p12Password:insecure:allowInvalidHostnameForTesting:keepAlive:cleanStart:cleanSession:sessionExpiryInterval:receiveMaximum:userProperties:willTopic:willPayloadBase64:willQos:willRetained:reconnectEnabled:reconnectInitialDelayMs:reconnectMaxDelayMs:reconnectJitterRatio:connectTimeoutMs:disconnectTimeoutMs:)
|
|
628
|
+
public init(clientId: String, host: String, port: NSNumber, protocolVersion: NSNumber, transport: String, path: String, username: String?, password: String?, caBase64: String?, p12Base64: String?, p12Password: String?, insecure: Bool, allowInvalidHostnameForTesting: Bool, keepAlive: NSNumber, cleanStart: Bool, cleanSession: Bool, sessionExpiryInterval: NSNumber, receiveMaximum: NSNumber, userProperties: [NSDictionary], willTopic: String?, willPayloadBase64: String?, willQos: NSNumber, willRetained: Bool, reconnectEnabled: Bool, reconnectInitialDelayMs: NSNumber, reconnectMaxDelayMs: NSNumber, reconnectJitterRatio: NSNumber, connectTimeoutMs: NSNumber, disconnectTimeoutMs: NSNumber) {
|
|
629
|
+
if protocolVersion.intValue == 4 {
|
|
630
|
+
self.transport = ReactNativeMqttCocoa3Transport(clientId: clientId, host: host, port: UInt16(truncating: port), transport: transport, path: path, username: username, password: password, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting, keepAlive: UInt16(truncating: keepAlive), cleanSession: cleanSession, willTopic: willTopic, willPayloadBase64: willPayloadBase64, willQos: UInt8(truncating: willQos), willRetained: willRetained)
|
|
631
|
+
} else {
|
|
632
|
+
self.transport = ReactNativeMqttCocoaTransport(clientId: clientId, host: host, port: UInt16(truncating: port), transport: transport, path: path, username: username, password: password, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting, keepAlive: UInt16(truncating: keepAlive), cleanStart: cleanStart, sessionExpiryInterval: UInt32(truncating: sessionExpiryInterval), receiveMaximum: UInt16(truncating: receiveMaximum), userProperties: userProperties, willTopic: willTopic, willPayloadBase64: willPayloadBase64, willQos: UInt8(truncating: willQos), willRetained: willRetained)
|
|
633
|
+
}
|
|
634
|
+
self.sessionTransport = ReactNativeMqttSessionTransportAdapter(underlying: self.transport)
|
|
635
|
+
self.session = ReactNativeMqttSession(
|
|
636
|
+
transport: self.sessionTransport,
|
|
637
|
+
queueLabel: "com.cnhongwei.reactnativemqtt.cocoa-session",
|
|
638
|
+
reconnectEnabled: reconnectEnabled,
|
|
639
|
+
reconnectInitialDelayMs: reconnectInitialDelayMs.uint64Value,
|
|
640
|
+
reconnectMaxDelayMs: reconnectMaxDelayMs.uint64Value,
|
|
641
|
+
reconnectJitterRatio: reconnectJitterRatio.doubleValue,
|
|
642
|
+
connectTimeoutMs: connectTimeoutMs.uint64Value,
|
|
643
|
+
disconnectTimeoutMs: disconnectTimeoutMs.uint64Value,
|
|
644
|
+
disconnectHandler: { [weak sessionTransport] error in sessionTransport?.publicDisconnectedHandler?(error) }
|
|
645
|
+
)
|
|
646
|
+
self.operationLedger = ReactNativeMqttOperationLedger(queueLabel: "com.cnhongwei.reactnativemqtt.cocoa-operations")
|
|
647
|
+
super.init()
|
|
648
|
+
session.setSnapshotHandler { [weak self] snapshot in
|
|
649
|
+
guard snapshot.state != .connected else { return }
|
|
650
|
+
self?.sessionEventHandler?(snapshot.state.rawValue, NSNumber(value: snapshot.revision), NSNumber(value: snapshot.generation), nil)
|
|
651
|
+
}
|
|
652
|
+
session.setConnectHandler { [weak self] snapshot, value in
|
|
653
|
+
guard let ack = value as? ReactNativeMqttCocoaConnAck else { return }
|
|
654
|
+
self?.sessionEventHandler?(snapshot.state.rawValue, NSNumber(value: snapshot.revision), NSNumber(value: snapshot.generation), [
|
|
655
|
+
"sessionPresent": ack.sessionPresent,
|
|
656
|
+
"reasonCode": Int(ack.reasonCode),
|
|
657
|
+
"reasonString": ack.reasonString as Any,
|
|
658
|
+
"assignedClientId": ack.assignedClientIdentifier as Any,
|
|
659
|
+
"userProperties": ack.userProperties.map { ["key": $0.key, "value": $0.value] },
|
|
660
|
+
])
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@objc(connectWithCompletion:)
|
|
665
|
+
public func connect(completion: @escaping ([String: Any]?, NSError?) -> Void) {
|
|
666
|
+
session.connect { result in
|
|
667
|
+
switch result {
|
|
668
|
+
case .success(let value):
|
|
669
|
+
guard let ack = value as? ReactNativeMqttCocoaConnAck else {
|
|
670
|
+
completion(nil, NSError(domain: "ReactNativeMqtt", code: 28, userInfo: [NSLocalizedDescriptionKey: "Invalid CONNACK result"]))
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
completion([
|
|
674
|
+
"sessionPresent": ack.sessionPresent,
|
|
675
|
+
"reasonCode": Int(ack.reasonCode),
|
|
676
|
+
"reasonString": ack.reasonString as Any,
|
|
677
|
+
"assignedClientId": ack.assignedClientIdentifier as Any,
|
|
678
|
+
"userProperties": ack.userProperties.map { ["key": $0.key, "value": $0.value] },
|
|
679
|
+
], nil)
|
|
680
|
+
case .failure(let error):
|
|
681
|
+
completion(nil, error as NSError)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
@objc public func disconnect() {
|
|
687
|
+
session.disconnect { _ in }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@objc(disconnectWithCompletion:)
|
|
691
|
+
public func disconnect(completion: @escaping (NSError?) -> Void) {
|
|
692
|
+
session.disconnect { error in completion(error as NSError?) }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@objc(disposeWithCompletion:)
|
|
696
|
+
public func dispose(completion: @escaping (NSError?) -> Void) {
|
|
697
|
+
session.dispose { error in completion(error as NSError?) }
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
@objc(setDisconnectedHandler:)
|
|
701
|
+
public func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
|
|
702
|
+
sessionTransport.publicDisconnectedHandler = { error in
|
|
703
|
+
let info = (error as NSError?)?.userInfo
|
|
704
|
+
handler(info?["reasonCode"] as? NSNumber, info?["reasonString"] as? String)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@objc(setSessionEventHandler:)
|
|
709
|
+
public func setSessionEventHandler(_ handler: @escaping (String, NSNumber, NSNumber, NSDictionary?) -> Void) {
|
|
710
|
+
sessionEventHandler = handler
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
@objc(setMessageHandler:)
|
|
714
|
+
public func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
|
|
715
|
+
transport.setMessageHandler(handler)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
@objc(subscribeFilter:qos:noLocal:retainAsPublished:retainHandling:completion:)
|
|
719
|
+
public func subscribe(filter: String, qos: NSNumber, noLocal: Bool, retainAsPublished: Bool, retainHandling: NSNumber, completion: @escaping (NSNumber?, NSError?) -> Void) {
|
|
720
|
+
operationLedger.submit(topic: filter, generation: session.snapshot.generation, timeoutMs: 30_000, execute: { [transport] finish in
|
|
721
|
+
transport.subscribe(filter: filter, qos: qos.uint8Value, noLocal: noLocal, retainAsPublished: retainAsPublished, retainHandling: retainHandling.uint8Value) { result in
|
|
722
|
+
switch result {
|
|
723
|
+
case .success(let granted): finish(.success(granted))
|
|
724
|
+
case .failure(let error): finish(.failure(error))
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}, completion: { result in
|
|
728
|
+
switch result {
|
|
729
|
+
case .success(let value): completion(value as? NSNumber, nil)
|
|
730
|
+
case .failure(let error): completion(nil, error as NSError)
|
|
731
|
+
}
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@objc(subscribeBatch:completion:)
|
|
736
|
+
public func subscribeBatch(_ subscriptions: [NSDictionary], completion: @escaping ([NSNumber]?, NSError?) -> Void) {
|
|
737
|
+
let parsed = subscriptions.compactMap { item -> (filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)? in
|
|
738
|
+
guard let filter = item["filter"] as? String,
|
|
739
|
+
let qos = item["qos"] as? NSNumber,
|
|
740
|
+
let noLocal = item["noLocal"] as? NSNumber,
|
|
741
|
+
let retainAsPublished = item["retainAsPublished"] as? NSNumber,
|
|
742
|
+
let retainHandling = item["retainHandling"] as? NSNumber else { return nil }
|
|
743
|
+
return (filter, qos.uint8Value, noLocal.boolValue, retainAsPublished.boolValue, retainHandling.uint8Value)
|
|
744
|
+
}
|
|
745
|
+
guard parsed.count == subscriptions.count else {
|
|
746
|
+
completion(nil, NSError(domain: "ReactNativeMqtt", code: 27, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription batch"])); return
|
|
747
|
+
}
|
|
748
|
+
transport.subscribeBatch(parsed) { result in
|
|
749
|
+
switch result {
|
|
750
|
+
case .success(let granted): completion(granted, nil)
|
|
751
|
+
case .failure(let error): completion(nil, error as NSError)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@objc(unsubscribeFilter:completion:)
|
|
757
|
+
public func unsubscribe(filter: String, completion: @escaping (NSError?) -> Void) {
|
|
758
|
+
operationLedger.submit(topic: filter, generation: session.snapshot.generation, timeoutMs: 30_000, execute: { [transport] finish in
|
|
759
|
+
transport.unsubscribe(filter: filter) { result in
|
|
760
|
+
switch result {
|
|
761
|
+
case .success: finish(.success(NSNull()))
|
|
762
|
+
case .failure(let error): finish(.failure(error))
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}, completion: { result in
|
|
766
|
+
switch result {
|
|
767
|
+
case .success: completion(nil)
|
|
768
|
+
case .failure(let error): completion(error as NSError)
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
@objc(publishTopic:payload:qos:retained:properties:completion:)
|
|
774
|
+
public func publish(topic: String, payload: NSData, qos: NSNumber, retained: Bool, properties: NSDictionary?, completion: @escaping (NSError?) -> Void) {
|
|
775
|
+
transport.publish(topic: topic, payload: [UInt8](payload as Data), qos: qos.uint8Value, retained: retained, properties: properties) { result in
|
|
776
|
+
switch result {
|
|
777
|
+
case .success: completion(nil)
|
|
778
|
+
case .failure(let error): completion(error as NSError)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|