@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.
Files changed (58) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +169 -2
  3. package/README.zh-CN.md +100 -0
  4. package/ReactNativeMqtt.podspec +28 -0
  5. package/THIRD_PARTY_NOTICES.md +17 -0
  6. package/android/build.gradle +70 -0
  7. package/android/consumer-rules.pro +10 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
  10. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
  11. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
  12. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
  13. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
  14. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
  15. package/ios/ReactNativeMqtt.h +15 -0
  16. package/ios/ReactNativeMqtt.mm +1184 -0
  17. package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
  18. package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
  19. package/ios/ReactNativeMqttSession.swift +420 -0
  20. package/lib/module/NativeReactNativeMqtt.js +5 -0
  21. package/lib/module/NativeReactNativeMqtt.js.map +1 -0
  22. package/lib/module/client.js +555 -0
  23. package/lib/module/client.js.map +1 -0
  24. package/lib/module/errors.js +20 -0
  25. package/lib/module/errors.js.map +1 -0
  26. package/lib/module/helpers.js +116 -0
  27. package/lib/module/helpers.js.map +1 -0
  28. package/lib/module/index.js +8 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/module/types.js +2 -0
  32. package/lib/module/types.js.map +1 -0
  33. package/lib/module/validation.js +306 -0
  34. package/lib/module/validation.js.map +1 -0
  35. package/lib/typescript/package.json +1 -0
  36. package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
  37. package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
  38. package/lib/typescript/src/client.d.ts +3 -0
  39. package/lib/typescript/src/client.d.ts.map +1 -0
  40. package/lib/typescript/src/errors.d.ts +18 -0
  41. package/lib/typescript/src/errors.d.ts.map +1 -0
  42. package/lib/typescript/src/helpers.d.ts +5 -0
  43. package/lib/typescript/src/helpers.d.ts.map +1 -0
  44. package/lib/typescript/src/index.d.ts +9 -0
  45. package/lib/typescript/src/index.d.ts.map +1 -0
  46. package/lib/typescript/src/types.d.ts +161 -0
  47. package/lib/typescript/src/types.d.ts.map +1 -0
  48. package/lib/typescript/src/validation.d.ts +13 -0
  49. package/lib/typescript/src/validation.d.ts.map +1 -0
  50. package/package.json +188 -4
  51. package/react-native.config.js +1 -0
  52. package/src/NativeReactNativeMqtt.ts +173 -0
  53. package/src/client.ts +859 -0
  54. package/src/errors.ts +51 -0
  55. package/src/helpers.ts +151 -0
  56. package/src/index.ts +30 -0
  57. package/src/types.ts +184 -0
  58. package/src/validation.ts +735 -0
@@ -0,0 +1,44 @@
1
+ package com.cnhongwei.reactnativemqtt;
2
+
3
+ import java.util.ResourceBundle;
4
+
5
+ /**
6
+ * Android-safe no-op logger for both direct Paho Java clients.
7
+ *
8
+ * Paho selects its logger by class name and otherwise loads a desktop logging
9
+ * implementation reflectively. Keeping this small implementation in the
10
+ * library avoids making Android consumers depend on that optional path.
11
+ */
12
+ public final class PahoLogger
13
+ implements org.eclipse.paho.client.mqttv3.logging.Logger,
14
+ org.eclipse.paho.mqttv5.client.logging.Logger {
15
+ public PahoLogger() {}
16
+ public void initialise(ResourceBundle c, String id, String r) {}
17
+ public void setResourceName(String r) {}
18
+ public boolean isLoggable(int l) { return false; }
19
+ public void severe(String a, String b, String c) {}
20
+ public void severe(String a, String b, String c, Object[] d) {}
21
+ public void severe(String a, String b, String c, Object[] d, Throwable e) {}
22
+ public void warning(String a, String b, String c) {}
23
+ public void warning(String a, String b, String c, Object[] d) {}
24
+ public void warning(String a, String b, String c, Object[] d, Throwable e) {}
25
+ public void info(String a, String b, String c) {}
26
+ public void info(String a, String b, String c, Object[] d) {}
27
+ public void info(String a, String b, String c, Object[] d, Throwable e) {}
28
+ public void config(String a, String b, String c) {}
29
+ public void config(String a, String b, String c, Object[] d) {}
30
+ public void config(String a, String b, String c, Object[] d, Throwable e) {}
31
+ public void fine(String a, String b, String c) {}
32
+ public void fine(String a, String b, String c, Object[] d) {}
33
+ public void fine(String a, String b, String c, Object[] d, Throwable e) {}
34
+ public void finer(String a, String b, String c) {}
35
+ public void finer(String a, String b, String c, Object[] d) {}
36
+ public void finer(String a, String b, String c, Object[] d, Throwable e) {}
37
+ public void finest(String a, String b, String c) {}
38
+ public void finest(String a, String b, String c, Object[] d) {}
39
+ public void finest(String a, String b, String c, Object[] d, Throwable e) {}
40
+ public void log(int a, String b, String c, String d, Object[] e, Throwable f) {}
41
+ public void trace(int a, String b, String c, String d, Object[] e, Throwable f) {}
42
+ public String formatMessage(String a, Object[] b) { return a; }
43
+ public void dumpTrace() {}
44
+ }
@@ -0,0 +1,399 @@
1
+ package com.cnhongwei.reactnativemqtt
2
+
3
+ import java.io.ByteArrayInputStream
4
+ import java.nio.ByteBuffer
5
+ import java.security.KeyStore
6
+ import java.security.Provider
7
+ import java.security.cert.CertificateFactory
8
+ import java.security.cert.X509Certificate
9
+ import javax.net.ssl.HttpsURLConnection
10
+ import java.util.concurrent.CompletableFuture
11
+ import java.util.concurrent.TimeUnit
12
+ import java.util.concurrent.TimeoutException
13
+ import javax.net.SocketFactory
14
+ import javax.net.ssl.HostnameVerifier
15
+ import javax.net.ssl.KeyManagerFactory
16
+ import javax.net.ssl.ManagerFactoryParameters
17
+ import javax.net.ssl.SSLContext
18
+ import javax.net.ssl.TrustManager
19
+ import javax.net.ssl.TrustManagerFactory
20
+ import javax.net.ssl.TrustManagerFactorySpi
21
+ import javax.net.ssl.X509TrustManager
22
+ import org.bouncycastle.jce.provider.BouncyCastleProvider
23
+ import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
24
+ import org.eclipse.paho.client.mqttv3.MqttAsyncClient
25
+ import org.eclipse.paho.client.mqttv3.MqttCallback
26
+ import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
27
+ import org.eclipse.paho.client.mqttv3.MqttConnectOptions
28
+ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
29
+ import org.eclipse.paho.client.mqttv3.MqttMessage
30
+ import org.eclipse.paho.mqttv5.client.IMqttMessageListener
31
+ import org.eclipse.paho.mqttv5.client.MqttCallback as Mqtt5Callback
32
+ import org.eclipse.paho.mqttv5.client.MqttConnectionOptions
33
+ import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse
34
+ import org.eclipse.paho.mqttv5.client.MqttAsyncClient as Mqtt5AsyncClient
35
+ import org.eclipse.paho.mqttv5.common.MqttException
36
+ import org.eclipse.paho.mqttv5.common.MqttMessage as Mqtt5Message
37
+ import org.eclipse.paho.mqttv5.common.MqttSubscription
38
+ import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence as Mqtt5MemoryPersistence
39
+ import org.eclipse.paho.mqttv5.common.packet.MqttProperties
40
+ import org.eclipse.paho.mqttv5.common.packet.MqttConnAck as PahoConnAck
41
+ import org.eclipse.paho.mqttv5.common.packet.MqttPubAck
42
+ import org.eclipse.paho.mqttv5.common.packet.MqttPubComp
43
+ import org.eclipse.paho.mqttv5.common.packet.MqttPubRec
44
+ import org.eclipse.paho.mqttv5.common.packet.MqttSubAck
45
+ import org.eclipse.paho.mqttv5.common.packet.MqttUnsubAck
46
+ import org.eclipse.paho.mqttv5.common.packet.UserProperty
47
+
48
+ internal fun decodeTlsBase64(encoded: String): ByteArray {
49
+ require(encoded.length % 4 == 0) { "TLS Base64 length must be a multiple of four" }
50
+ if (encoded.isEmpty()) return ByteArray(0)
51
+ fun value(c: Char): Int = when (c) {
52
+ in 'A'..'Z' -> c.code - 'A'.code
53
+ in 'a'..'z' -> c.code - 'a'.code + 26
54
+ in '0'..'9' -> c.code - '0'.code + 52
55
+ '+' -> 62
56
+ '/' -> 63
57
+ else -> -1
58
+ }
59
+ val padding = if (encoded.endsWith("==")) 2 else if (encoded.endsWith("=")) 1 else 0
60
+ val output = ByteArray(encoded.length / 4 * 3 - padding)
61
+ var out = 0
62
+ var i = 0
63
+ while (i < encoded.length) {
64
+ val a = value(encoded[i]); val b = value(encoded[i + 1])
65
+ val c = if (encoded[i + 2] == '=') -2 else value(encoded[i + 2])
66
+ val d = if (encoded[i + 3] == '=') -2 else value(encoded[i + 3])
67
+ require(a >= 0 && b >= 0 && (c == -2 || c >= 0) && (d == -2 || d >= 0) && !(c == -2 && d != -2)) { "TLS Base64 is invalid" }
68
+ require(i + 4 == encoded.length || (c >= 0 && d >= 0)) { "TLS Base64 padding must be final" }
69
+ if (c == -2) require((b and 15) == 0) { "TLS Base64 has non-zero trailing bits" }
70
+ if (d == -2 && c >= 0) require((c and 3) == 0) { "TLS Base64 has non-zero trailing bits" }
71
+ val bits = (a shl 18) or (b shl 12) or ((if (c >= 0) c else 0) shl 6) or (if (d >= 0) d else 0)
72
+ if (out < output.size) output[out++] = (bits ushr 16).toByte()
73
+ if (c >= 0 && out < output.size) output[out++] = (bits ushr 8).toByte()
74
+ if (d >= 0 && out < output.size) output[out++] = bits.toByte()
75
+ i += 4
76
+ }
77
+ return output
78
+ }
79
+
80
+ internal data class MqttConnAck(
81
+ val sessionPresent: Boolean,
82
+ val reasonCode: Int? = null,
83
+ val reasonString: String? = null,
84
+ val assignedClientId: String? = null,
85
+ val userProperties: List<Map<String, String>> = emptyList(),
86
+ )
87
+ internal data class MqttDisconnect(val reasonCode: Int, val reasonString: String?)
88
+ internal data class MqttSubscriptionResult(val grantedQos: Int, val reasonCode: Int)
89
+ internal data class MqttSubscriptionRequest(val filter: String, val qos: Int, val noLocal: Boolean, val retainAsPublished: Boolean, val retainHandling: Int)
90
+ internal data class MqttPublishProperties(
91
+ val userProperties: List<Pair<String, String>> = emptyList(), val contentType: String? = null,
92
+ val responseTopic: String? = null, val correlationData: ByteArray? = null,
93
+ val messageExpiryIntervalSeconds: Long? = null, val payloadFormatIndicator: Int? = null,
94
+ )
95
+ internal data class MqttMessageData(
96
+ val topic: String, val payload: ByteArray, val qos: Int, val retained: Boolean,
97
+ val userProperties: List<Map<String, String>> = emptyList(), val contentType: String? = null,
98
+ val responseTopic: String? = null, val correlationData: ByteArray? = null,
99
+ val messageExpiryIntervalSeconds: Long? = null, val payloadFormatIndicator: Int? = null,
100
+ )
101
+
102
+ internal interface MqttTransport {
103
+ fun connect(): CompletableFuture<MqttConnAck>
104
+ /** True when a lost network connection can be re-established on this client. */
105
+ fun supportsInPlaceReconnect(): Boolean = false
106
+ fun disconnect(): CompletableFuture<Void>
107
+ fun subscribe(filter: String, qos: Int, noLocal: Boolean, retainAsPublished: Boolean, retainHandling: Int, onMessage: ((MqttMessageData) -> Unit)? = null): CompletableFuture<MqttSubscriptionResult>
108
+ fun subscribeBatch(requests: List<MqttSubscriptionRequest>): CompletableFuture<List<MqttSubscriptionResult>> {
109
+ if (requests.isEmpty()) return CompletableFuture.completedFuture(emptyList())
110
+ val futures = requests.map { subscribe(it.filter, it.qos, it.noLocal, it.retainAsPublished, it.retainHandling) }
111
+ return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.map { it.join() } }
112
+ }
113
+ fun unsubscribe(filter: String): CompletableFuture<Int>
114
+ fun publish(topic: String, payload: ByteArray, qos: Int, retained: Boolean): CompletableFuture<Void>
115
+ fun publish(topic: String, payload: ByteArray, qos: Int, retained: Boolean, properties: MqttPublishProperties): CompletableFuture<Void> = publish(topic, payload, qos, retained)
116
+ fun setDisconnectedListener(listener: (MqttDisconnect?) -> Unit)
117
+ }
118
+
119
+ internal class InsecureTrustManagerFactorySpi : TrustManagerFactorySpi() {
120
+ override fun engineInit(keystore: KeyStore?) = Unit
121
+ override fun engineInit(spec: ManagerFactoryParameters?) = Unit
122
+ override fun engineGetTrustManagers(): Array<TrustManager> = arrayOf(object : X509TrustManager {
123
+ override fun getAcceptedIssuers() = emptyArray<X509Certificate>()
124
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
125
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
126
+ })
127
+ }
128
+
129
+ private fun scopedMosquittoHostnameVerifier(expectedHost: String): HostnameVerifier = HostnameVerifier { host, session ->
130
+ if (!expectedHost.equals("test.mosquitto.org", ignoreCase = true) || !host.equals(expectedHost, ignoreCase = true)) return@HostnameVerifier false
131
+ val leaf = runCatching { session.peerCertificates.firstOrNull() as? X509Certificate }.getOrNull() ?: return@HostnameVerifier false
132
+ val sans = runCatching { leaf.subjectAlternativeNames.orEmpty().filter { it.size >= 2 && it[0] == 2 }.mapNotNull { it[1] as? String } }.getOrDefault(emptyList())
133
+ if (sans.isNotEmpty()) return@HostnameVerifier HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
134
+ val commonName = Regex("(?:^|,)\\s*CN=([^,]+)", RegexOption.IGNORE_CASE)
135
+ .find(leaf.subjectX500Principal.name)
136
+ ?.groupValues
137
+ ?.getOrNull(1)
138
+ ?.trim()
139
+ commonName?.equals(host, ignoreCase = true) == true
140
+ }
141
+
142
+ internal class PahoMqttTransport(private val options: ParsedMqttOptions, private val onMessage: (MqttMessageData) -> Unit = {}) : MqttTransport {
143
+ companion object {
144
+ init {
145
+ org.eclipse.paho.client.mqttv3.logging.LoggerFactory.setLogger(PahoLogger::class.java.name)
146
+ org.eclipse.paho.mqttv5.client.logging.LoggerFactory.setLogger(PahoLogger::class.java.name)
147
+ }
148
+ }
149
+
150
+ private var mqtt3: MqttAsyncClient? = null
151
+ private var mqtt5: Mqtt5AsyncClient? = null
152
+ @Volatile private var reconnecting3: CompletableFuture<MqttConnAck>? = null
153
+ @Volatile private var reconnecting5: CompletableFuture<MqttConnAck>? = null
154
+ @Volatile private var connected3 = false
155
+ @Volatile private var connected5 = false
156
+ private var disconnectedListener: (MqttDisconnect?) -> Unit = {}
157
+
158
+ override fun setDisconnectedListener(listener: (MqttDisconnect?) -> Unit) { disconnectedListener = listener }
159
+ override fun supportsInPlaceReconnect(): Boolean = true
160
+ override fun connect(): CompletableFuture<MqttConnAck> = if (options.protocolVersion == 5) connect5() else connect3()
161
+ override fun disconnect(): CompletableFuture<Void> = try {
162
+ when {
163
+ mqtt5 != null -> disconnect5(mqtt5!!)
164
+ mqtt3 != null -> disconnect3(mqtt3!!)
165
+ else -> CompletableFuture.completedFuture(null)
166
+ }
167
+ } catch (error: Throwable) { failed("ERR_DISCONNECT", error.message ?: "Paho disconnect failed") }
168
+
169
+ override fun subscribe(filter: String, qos: Int, noLocal: Boolean, retainAsPublished: Boolean, retainHandling: Int, onMessage: ((MqttMessageData) -> Unit)?): CompletableFuture<MqttSubscriptionResult> {
170
+ val callback = onMessage ?: this.onMessage
171
+ return try {
172
+ if (options.protocolVersion == 5) {
173
+ val subscription = MqttSubscription(filter, qos).also { it.isNoLocal = noLocal; it.isRetainAsPublished = retainAsPublished; it.retainHandling = retainHandling }
174
+ val token = mqtt5?.subscribe(subscription) ?: return failed("ERR_NOT_CONNECTED", "MQTT 5 client is not connected")
175
+ tokenFuture(token, token).thenApply { ack -> ensureV5Success(ack); MqttSubscriptionResult(ack.getGrantedQos().firstOrNull() ?: -1, ack.getReasonCodes().firstOrNull() ?: -1) }
176
+ } else {
177
+ val token = mqtt3?.subscribe(filter, qos, org.eclipse.paho.client.mqttv3.IMqttMessageListener { topic, message -> callback(topic.toMessageData(message)) })
178
+ ?: return failed("ERR_NOT_CONNECTED", "MQTT 3.1.1 client is not connected")
179
+ tokenFuture(token, token).thenApply { ack -> MqttSubscriptionResult(ack.getGrantedQos().firstOrNull() ?: -1, ack.getGrantedQos().firstOrNull() ?: -1) }
180
+ }
181
+ } catch (error: Throwable) { failed("ERR_SUBSCRIBE", error.message ?: "Paho subscribe failed") }
182
+ }
183
+
184
+ override fun subscribeBatch(requests: List<MqttSubscriptionRequest>): CompletableFuture<List<MqttSubscriptionResult>> {
185
+ if (requests.isEmpty()) return CompletableFuture.completedFuture(emptyList())
186
+ return try {
187
+ if (options.protocolVersion == 5) {
188
+ val subscriptions = requests.map { request -> MqttSubscription(request.filter, request.qos).also { it.isNoLocal = request.noLocal; it.isRetainAsPublished = request.retainAsPublished; it.retainHandling = request.retainHandling } }.toTypedArray()
189
+ val token = mqtt5?.subscribe(subscriptions) ?: return failed("ERR_NOT_CONNECTED", "MQTT 5 client is not connected")
190
+ tokenFuture(token, token).thenApply { ack ->
191
+ ensureV5Success(ack)
192
+ require(ack.getReasonCodes().size == requests.size) { "SUBACK item count did not match request" }
193
+ ack.getReasonCodes().mapIndexed { index, reason -> MqttSubscriptionResult(ack.getGrantedQos().getOrElse(index) { -1 }, reason) }
194
+ }
195
+ } else {
196
+ val token = mqtt3?.subscribe(requests.map { it.filter }.toTypedArray(), requests.map { it.qos }.toIntArray()) ?: return failed("ERR_NOT_CONNECTED", "MQTT 3.1.1 client is not connected")
197
+ tokenFuture(token, Unit).thenApply {
198
+ require(token.getGrantedQos().size == requests.size) { "SUBACK item count did not match request" }
199
+ token.getGrantedQos().map { MqttSubscriptionResult(it, it) }
200
+ }
201
+ }
202
+ } catch (error: Throwable) { failed("ERR_SUBSCRIBE", error.message ?: "Paho subscribe failed") }
203
+ }
204
+
205
+ override fun unsubscribe(filter: String): CompletableFuture<Int> = try {
206
+ if (options.protocolVersion == 5) {
207
+ val token = mqtt5?.unsubscribe(filter) ?: return failed("ERR_NOT_CONNECTED", "MQTT 5 client is not connected")
208
+ tokenFuture(token, Unit).thenApply { ensureV5Success(token); token.getReasonCodes().firstOrNull() ?: -1 }
209
+ } else {
210
+ val token = mqtt3?.unsubscribe(filter) ?: return failed("ERR_NOT_CONNECTED", "MQTT 3.1.1 client is not connected")
211
+ tokenFuture(token, 0)
212
+ }
213
+ } catch (error: Throwable) { failed("ERR_UNSUBSCRIBE", error.message ?: "Paho unsubscribe failed") }
214
+
215
+ override fun publish(topic: String, payload: ByteArray, qos: Int, retained: Boolean): CompletableFuture<Void> = publish(topic, payload, qos, retained, MqttPublishProperties())
216
+ override fun publish(topic: String, payload: ByteArray, qos: Int, retained: Boolean, properties: MqttPublishProperties): CompletableFuture<Void> {
217
+ if (options.protocolVersion != 5 && (properties.userProperties.isNotEmpty() || properties.contentType != null || properties.responseTopic != null || properties.correlationData != null || properties.messageExpiryIntervalSeconds != null || properties.payloadFormatIndicator != null)) return failed("ERR_INVALID_OPTIONS", "MQTT 5 publish properties are not valid for MQTT 3.1.1")
218
+ return try {
219
+ if (options.protocolVersion == 5) {
220
+ val message = Mqtt5Message(payload, qos, retained, properties.toPaho())
221
+ val token = mqtt5?.publish(topic, message) ?: return failed("ERR_NOT_CONNECTED", "MQTT 5 client is not connected")
222
+ tokenFuture(token, Unit).thenApply { ensureV5Success(token); null }
223
+ } else {
224
+ val message = MqttMessage(payload).also { it.qos = qos; it.isRetained = retained }
225
+ val token = mqtt3?.publish(topic, message) ?: return failed("ERR_NOT_CONNECTED", "MQTT 3.1.1 client is not connected")
226
+ tokenFuture(token, Unit).thenApply { null }
227
+ }
228
+ } catch (error: Throwable) { failed("ERR_PUBLISH_FAILED", error.message ?: "Paho publish failed") }
229
+ }
230
+
231
+ private fun connect3(): CompletableFuture<MqttConnAck> = try {
232
+ val client = mqtt3 ?: MqttAsyncClient(uri(), options.clientId, MemoryPersistence()).also { created ->
233
+ created.setCallback(object : MqttCallbackExtended {
234
+ override fun connectionLost(cause: Throwable?) { disconnectedListener(null) }
235
+ override fun messageArrived(topic: String, message: MqttMessage) { onMessage(topic.toMessageData(message)) }
236
+ override fun deliveryComplete(token: IMqttDeliveryToken) = Unit
237
+ override fun connectComplete(reconnect: Boolean, serverURI: String) {
238
+ if (reconnect) reconnecting3?.complete(MqttConnAck(true, 0))
239
+ }
240
+ })
241
+ mqtt3 = created
242
+ }
243
+ if (!client.isConnected && reconnecting3 == null && connected3) {
244
+ CompletableFuture<MqttConnAck>().also { waiter ->
245
+ reconnecting3 = waiter
246
+ try {
247
+ client.reconnect()
248
+ CompletableFuture.runAsync { awaitReconnect(client.isConnected) { client.isConnected } }
249
+ .whenComplete { _, error -> if (error != null) waiter.completeExceptionally(error) else waiter.complete(MqttConnAck(true, 0)) }
250
+ } catch (error: Throwable) {
251
+ reconnecting3 = null
252
+ waiter.completeExceptionally(error)
253
+ }
254
+ }.whenComplete { _, _ -> reconnecting3 = null }
255
+ } else {
256
+ val connect = MqttConnectOptions().apply {
257
+ mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1; isCleanSession = options.cleanSession; keepAliveInterval = options.keepAliveSeconds; connectionTimeout = options.connectTimeoutSeconds(); isAutomaticReconnect = false
258
+ options.username?.let { userName = it }; options.password?.let { password = it.toCharArray() }; socketFactory()?.let { setSocketFactory(it) }; hostnameVerifier()?.let { setSSLHostnameVerifier(it) }; setHttpsHostnameVerificationEnabled(!options.tlsInsecure && !options.tlsAllowInvalidHostnameForTesting)
259
+ options.willTopic?.let { setWill(it, decodeStrictBase64(options.willPayloadBase64!!), options.willQos, options.willRetained) }
260
+ }
261
+ val connectToken = client.connect(connect)
262
+ tokenFuture(connectToken, connectToken).thenApply { token ->
263
+ connected3 = true
264
+ MqttConnAck(token.getSessionPresent())
265
+ }
266
+ }
267
+ } catch (error: Throwable) { failed("ERR_CONNECT", error.message ?: "Paho MQTT 3.1.1 connect failed") }
268
+
269
+ private fun disconnect3(client: MqttAsyncClient): CompletableFuture<Void> = CompletableFuture.supplyAsync {
270
+ try {
271
+ client.disconnect(1000).waitForCompletion(2000)
272
+ } catch (_: Throwable) {
273
+ runCatching { client.disconnectForcibly() }
274
+ } finally {
275
+ runCatching { client.close() }
276
+ if (mqtt3 === client) mqtt3 = null
277
+ }
278
+ null
279
+ }
280
+
281
+ private fun connect5(): CompletableFuture<MqttConnAck> = try {
282
+ val client = mqtt5 ?: Mqtt5AsyncClient(uri(), options.clientId, Mqtt5MemoryPersistence()).also { created ->
283
+ created.setCallback(object : Mqtt5Callback {
284
+ override fun disconnected(response: MqttDisconnectResponse) { disconnectedListener(MqttDisconnect(response.returnCode, response.reasonString)) }
285
+ override fun mqttErrorOccurred(exception: MqttException) = Unit
286
+ override fun messageArrived(topic: String, message: Mqtt5Message) { onMessage(topic.toMessageData(message)) }
287
+ override fun deliveryComplete(token: org.eclipse.paho.mqttv5.client.IMqttToken) = Unit
288
+ override fun connectComplete(reconnect: Boolean, serverURI: String) {
289
+ if (reconnect) reconnecting5?.complete(MqttConnAck(true, 0))
290
+ }
291
+ override fun authPacketArrived(reasonCode: Int, properties: MqttProperties) = Unit
292
+ })
293
+ mqtt5 = created
294
+ }
295
+ if (!client.isConnected && reconnecting5 == null && connected5) {
296
+ CompletableFuture<MqttConnAck>().also { waiter ->
297
+ reconnecting5 = waiter
298
+ try {
299
+ client.reconnect()
300
+ CompletableFuture.runAsync { awaitReconnect(client.isConnected) { client.isConnected } }
301
+ .whenComplete { _, error -> if (error != null) waiter.completeExceptionally(error) else waiter.complete(MqttConnAck(true, 0)) }
302
+ } catch (error: Throwable) {
303
+ reconnecting5 = null
304
+ waiter.completeExceptionally(error)
305
+ }
306
+ }.whenComplete { _, _ -> reconnecting5 = null }
307
+ } else {
308
+ val connect = MqttConnectionOptions().apply {
309
+ setCleanStart(options.cleanStart); keepAliveInterval = options.keepAliveSeconds; connectionTimeout = options.connectTimeoutSeconds(); isAutomaticReconnect = false; setSessionExpiryInterval(options.sessionExpiryIntervalSeconds); setReceiveMaximum(options.receiveMaximum)
310
+ options.username?.let { userName = it }; options.password?.let { password = it.toByteArray(Charsets.UTF_8) }; socketFactory()?.let { setSocketFactory(it) }; hostnameVerifier()?.let { setSSLHostnameVerifier(it) }; setHttpsHostnameVerificationEnabled(!options.tlsInsecure && !options.tlsAllowInvalidHostnameForTesting)
311
+ options.userProperties.takeIf { it.isNotEmpty() }?.let { userProperties = it.map { p -> UserProperty(p.first, p.second) } }
312
+ options.willTopic?.let { setWill(it, Mqtt5Message(decodeStrictBase64(options.willPayloadBase64!!), options.willQos, options.willRetained, MqttProperties())) }
313
+ }
314
+ val connectToken = client.connect(connect)
315
+ tokenFuture(connectToken, connectToken).thenApply { token ->
316
+ ensureV5Success(token)
317
+ connected5 = true
318
+ val properties = token.getResponseProperties()
319
+ MqttConnAck(token.getSessionPresent(), token.getReasonCodes()?.firstOrNull() ?: 0, properties?.getReasonString(), properties?.getAssignedClientIdentifier(), properties?.getUserProperties()?.map { mapOf("key" to it.key, "value" to it.value) } ?: emptyList())
320
+ }
321
+ }
322
+ } catch (error: Throwable) { failed("ERR_CONNECT", error.message ?: "Paho MQTT 5 connect failed") }
323
+
324
+ private fun disconnect5(client: Mqtt5AsyncClient): CompletableFuture<Void> = CompletableFuture.supplyAsync {
325
+ try {
326
+ client.disconnect(1000).waitForCompletion(2000)
327
+ } catch (_: Throwable) {
328
+ runCatching { client.disconnectForcibly() }
329
+ } finally {
330
+ runCatching { client.close() }
331
+ if (mqtt5 === client) mqtt5 = null
332
+ }
333
+ null
334
+ }
335
+
336
+ private fun uri() = "${when (options.transport) {
337
+ "tls" -> "ssl"
338
+ "ws", "wss" -> options.transport
339
+ else -> "tcp"
340
+ }}://${options.host}:${options.port}${if (options.transport == "ws" || options.transport == "wss") options.path else ""}"
341
+ private fun awaitReconnect(initiallyConnected: Boolean, connected: () -> Boolean) {
342
+ if (initiallyConnected) return
343
+ val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(options.connectTimeoutSeconds().toLong())
344
+ while (!connected()) {
345
+ if (System.nanoTime() >= deadline) throw TimeoutException("Paho reconnect timed out")
346
+ Thread.sleep(10)
347
+ }
348
+ }
349
+ private fun <T> tokenFuture(token: org.eclipse.paho.mqttv5.client.IMqttToken, value: T): CompletableFuture<T> = CompletableFuture.supplyAsync { token.waitForCompletion(); value }
350
+ private fun ensureV5Success(token: org.eclipse.paho.mqttv5.client.IMqttToken) {
351
+ token.getException()?.let { throw it }
352
+ val codes = token.getReasonCodes()?.takeIf { it.isNotEmpty() } ?: when (val response = token.getResponse()) {
353
+ is PahoConnAck -> intArrayOf(response.returnCode)
354
+ is MqttPubAck -> intArrayOf(response.returnCode)
355
+ is MqttPubRec -> intArrayOf(response.returnCode)
356
+ is MqttPubComp -> intArrayOf(response.returnCode)
357
+ is MqttSubAck -> response.returnCodes
358
+ is MqttUnsubAck -> response.returnCodes
359
+ else -> null
360
+ }
361
+ codes?.firstOrNull { it >= 0x80 }?.let { throw MqttException(it) }
362
+ }
363
+ private fun <T> tokenFuture(token: org.eclipse.paho.client.mqttv3.IMqttToken, value: T): CompletableFuture<T> = CompletableFuture.supplyAsync { token.waitForCompletion(); value }
364
+ private fun <T> failed(code: String, message: String): CompletableFuture<T> = CompletableFuture<T>().also { it.completeExceptionally(IllegalStateException("$code: $message")) }
365
+ private fun ParsedMqttOptions.connectTimeoutSeconds() = (connectTimeoutMs / 1000).coerceAtLeast(1).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
366
+ private fun hostnameVerifier(): HostnameVerifier? = when {
367
+ options.tlsInsecure -> HostnameVerifier { _, _ -> true }
368
+ options.tlsAllowInvalidHostnameForTesting -> scopedMosquittoHostnameVerifier(options.host)
369
+ else -> null
370
+ }
371
+ private fun socketFactory(): SocketFactory? = if (options.transport == "tls" || options.transport == "wss") sslContext().socketFactory else null
372
+
373
+ private fun sslContext(): SSLContext = SSLContext.getInstance("TLS").also { context ->
374
+ val trust = if (options.tlsInsecure && options.tlsCaBase64 == null) arrayOf<TrustManager>(object : X509TrustManager { override fun getAcceptedIssuers() = emptyArray<X509Certificate>(); override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit; override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit }) else TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).also { factory ->
375
+ if (options.tlsCaBase64 == null) {
376
+ factory.init(null as KeyStore?)
377
+ } else {
378
+ // Use the same bundled provider as the client-certificate path. Android's
379
+ // platform PKIX implementation can ignore a certificate-only PKCS12 store
380
+ // on some API/provider combinations, leaving no trust anchor at runtime.
381
+ val store = KeyStore.getInstance("BKS", BouncyCastleProvider()).apply { load(null, null) }
382
+ store.setCertificateEntry("react-native-mqtt-ca", CertificateFactory.getInstance("X.509").generateCertificate(ByteArrayInputStream(decodeTlsBase64(options.tlsCaBase64))))
383
+ factory.init(store)
384
+ }
385
+ }.trustManagers
386
+ val key = options.tlsP12Base64?.let { encoded ->
387
+ val store = KeyStore.getInstance("PKCS12", BouncyCastleProvider()).apply { load(ByteArrayInputStream(decodeTlsBase64(encoded)), options.tlsP12Password?.toCharArray()) }
388
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).also { it.init(store, options.tlsP12Password?.toCharArray()) }.keyManagers
389
+ }
390
+ context.init(key, trust, null)
391
+ }
392
+
393
+ private fun MqttPublishProperties.toPaho() = MqttProperties().also { p ->
394
+ p.userProperties = userProperties.map { UserProperty(it.first, it.second) }; contentType?.let { p.contentType = it }; responseTopic?.let { p.responseTopic = it }; correlationData?.let { p.correlationData = it }; messageExpiryIntervalSeconds?.let { p.messageExpiryInterval = it }; payloadFormatIndicator?.let { p.payloadFormat = it != 0 }
395
+ }
396
+ private fun String.toMessageData(message: Mqtt5Message) = MqttMessageData(this, message.payload, message.qos, message.isRetained, message.properties.userProperties.map { mapOf("key" to it.key, "value" to it.value) }, message.properties.contentType, message.properties.responseTopic, message.properties.correlationData, message.properties.messageExpiryInterval, if (message.properties.getPayloadFormat()) 1 else 0)
397
+ private fun String.toMessageData(message: MqttMessage) = MqttMessageData(this, message.payload, message.qos, message.isRetained)
398
+ private fun ByteBuffer.toByteArray() = duplicate().let { copy -> ByteArray(copy.remaining()).also(copy::get) }
399
+ }