@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,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
|
+
}
|