@annadata/capacitor-mqtt-quic 0.1.7 → 0.1.9
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/AnnadataCapacitorMqttQuic.podspec +2 -2
- package/Package.swift +59 -0
- package/README.md +57 -17
- package/android/NGTCP2_BUILD_INSTRUCTIONS.md +1 -1
- package/android/app/src/main/assets/capacitor.config.json +1 -1
- package/android/build-wolfssl.sh +8 -2
- package/android/build.gradle +4 -1
- package/android/install/nghttp3-android/arm64-v8a/lib/libnghttp3.a +0 -0
- package/android/install/nghttp3-android/arm64-v8a/lib/libnghttp3.so +0 -0
- package/android/install/nghttp3-android/arm64-v8a/lib/pkgconfig/libnghttp3.pc +4 -4
- package/android/install/nghttp3-android/armeabi-v7a/lib/libnghttp3.a +0 -0
- package/android/install/nghttp3-android/armeabi-v7a/lib/libnghttp3.so +0 -0
- package/android/install/nghttp3-android/armeabi-v7a/lib/pkgconfig/libnghttp3.pc +4 -4
- package/android/install/nghttp3-android/x86_64/lib/libnghttp3.a +0 -0
- package/android/install/nghttp3-android/x86_64/lib/libnghttp3.so +0 -0
- package/android/install/nghttp3-android/x86_64/lib/pkgconfig/libnghttp3.pc +4 -4
- package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2.a +0 -0
- package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2.so +0 -0
- package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2_crypto_wolfssl.a +0 -0
- package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2_crypto_wolfssl.so +0 -0
- package/android/install/ngtcp2-android/arm64-v8a/lib/pkgconfig/libngtcp2.pc +4 -4
- package/android/install/ngtcp2-android/arm64-v8a/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
- package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2.a +0 -0
- package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2.so +0 -0
- package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2_crypto_wolfssl.a +0 -0
- package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2_crypto_wolfssl.so +0 -0
- package/android/install/ngtcp2-android/armeabi-v7a/lib/pkgconfig/libngtcp2.pc +4 -4
- package/android/install/ngtcp2-android/armeabi-v7a/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
- package/android/install/ngtcp2-android/x86_64/lib/libngtcp2.a +0 -0
- package/android/install/ngtcp2-android/x86_64/lib/libngtcp2.so +0 -0
- package/android/install/ngtcp2-android/x86_64/lib/libngtcp2_crypto_wolfssl.a +0 -0
- package/android/install/ngtcp2-android/x86_64/lib/libngtcp2_crypto_wolfssl.so +0 -0
- package/android/install/ngtcp2-android/x86_64/lib/pkgconfig/libngtcp2.pc +4 -4
- package/android/install/ngtcp2-android/x86_64/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
- package/android/install/wolfssl-android/arm64-v8a/bin/wolfssl-config +1 -1
- package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.la +1 -1
- package/android/install/wolfssl-android/arm64-v8a/lib/pkgconfig/wolfssl.pc +1 -1
- package/android/install/wolfssl-android/armeabi-v7a/bin/wolfssl-config +1 -1
- package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.la +1 -1
- package/android/install/wolfssl-android/armeabi-v7a/lib/pkgconfig/wolfssl.pc +1 -1
- package/android/install/wolfssl-android/x86_64/bin/wolfssl-config +1 -1
- package/android/install/wolfssl-android/x86_64/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/x86_64/lib/libwolfssl.la +1 -1
- package/android/install/wolfssl-android/x86_64/lib/pkgconfig/wolfssl.pc +1 -1
- package/android/src/main/cpp/CMakeLists.txt +19 -5
- package/android/src/main/cpp/ngtcp2_jni.cpp +119 -32
- package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +60 -5
- package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +233 -84
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocol.kt +36 -5
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTTypes.kt +15 -15
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/NGTCP2Client.kt +26 -15
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicClientStub.kt +1 -1
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicTypes.kt +1 -1
- package/android/src/main/kotlin/ai/annadata/mqttquic/transport/QUICStreamAdapter.kt +80 -5
- package/android/src/main/kotlin/ai/annadata/mqttquic/transport/StreamTransport.kt +4 -0
- package/docs/IMPLEMENTATION_SUMMARY.md +1 -1
- package/docs/NGTCP2_IMPLEMENTATION_STATUS.md +1 -1
- package/docs/PRODUCTION_PUBLISH_STEPS.md +9 -3
- package/docs/diff-node_modules-vs-standalone-android-src.patch +1031 -0
- package/ios/{MqttQuicPlugin.podspec → AnnadataCapacitorMqttQuic.podspec} +4 -4
- package/ios/App/App/capacitor.config.json +1 -1
- package/ios/NGTCP2_BUILD_INSTRUCTIONS.md +3 -3
- package/ios/Package.swift +7 -8
- package/ios/Tests/MQTTProtocolTests.swift +1 -1
- package/ios/build-wolfssl.sh +8 -3
- package/ios/libs/libnghttp3.a +0 -0
- package/ios/libs/libngtcp2.a +0 -0
- package/ios/libs/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs/libwolfssl.a +0 -0
- package/ios/libs-simulator/libnghttp3.a +0 -0
- package/ios/libs-simulator/libngtcp2.a +0 -0
- package/ios/libs-simulator/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator/libwolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libnghttp3.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libwolfssl.a +0 -0
- package/package.json +5 -2
- package/ios/libs/MqttQuicLibs.xcframework/Info.plist +0 -44
- package/ios/libs/MqttQuicLibs.xcframework/ios-arm64/libmqttquic_native_device.a +0 -0
- package/ios/libs/MqttQuicLibs.xcframework/ios-arm64_x86_64-simulator/libmqttquic_native_simulator.a +0 -0
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
#include <algorithm>
|
|
25
25
|
#include <atomic>
|
|
26
26
|
#include <chrono>
|
|
27
|
+
#include <cinttypes>
|
|
27
28
|
#include <cstdarg>
|
|
28
29
|
#include <cstdlib>
|
|
29
30
|
#include <cstdio>
|
|
@@ -73,7 +74,11 @@ struct OutgoingChunk {
|
|
|
73
74
|
class QuicClient {
|
|
74
75
|
public:
|
|
75
76
|
QuicClient(std::string host, uint16_t port)
|
|
76
|
-
:
|
|
77
|
+
: QuicClient(std::move(host), "", port) {}
|
|
78
|
+
|
|
79
|
+
QuicClient(std::string host_for_tls, std::string connect_addr, uint16_t port)
|
|
80
|
+
: host_(std::move(host_for_tls)),
|
|
81
|
+
connect_addr_(connect_addr.empty() ? host_ : std::move(connect_addr)),
|
|
77
82
|
port_(port),
|
|
78
83
|
fd_(-1),
|
|
79
84
|
ssl_ctx_(nullptr),
|
|
@@ -181,6 +186,9 @@ class QuicClient {
|
|
|
181
186
|
buffer[i] = state.recv_buf.front();
|
|
182
187
|
state.recv_buf.pop_front();
|
|
183
188
|
}
|
|
189
|
+
if (n > 0) {
|
|
190
|
+
LOGI("read_stream stream_id=%" PRId64 " returning %zu bytes", (int64_t)stream_id, n);
|
|
191
|
+
}
|
|
184
192
|
return (ssize_t)n;
|
|
185
193
|
}
|
|
186
194
|
|
|
@@ -188,7 +196,7 @@ class QuicClient {
|
|
|
188
196
|
if (!conn_) {
|
|
189
197
|
return 0;
|
|
190
198
|
}
|
|
191
|
-
int rv = ngtcp2_conn_shutdown_stream_write(conn_, stream_id, 0);
|
|
199
|
+
int rv = ngtcp2_conn_shutdown_stream_write(conn_, 0, stream_id, 0);
|
|
192
200
|
if (rv != 0) {
|
|
193
201
|
setError(ngtcp2_strerror(rv));
|
|
194
202
|
return -1;
|
|
@@ -223,6 +231,8 @@ class QuicClient {
|
|
|
223
231
|
std::lock_guard<std::mutex> lock(stream_mutex_);
|
|
224
232
|
StreamState &state = streams_[stream_id];
|
|
225
233
|
state.recv_buf.insert(state.recv_buf.end(), data, data + datalen);
|
|
234
|
+
LOGI("recv stream data stream_id=%" PRId64 " len=%zu recv_buf_total=%zu",
|
|
235
|
+
(int64_t)stream_id, datalen, state.recv_buf.size());
|
|
226
236
|
if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) {
|
|
227
237
|
state.fin_received = true;
|
|
228
238
|
}
|
|
@@ -254,7 +264,8 @@ class QuicClient {
|
|
|
254
264
|
|
|
255
265
|
char port_str[16];
|
|
256
266
|
snprintf(port_str, sizeof(port_str), "%u", port_);
|
|
257
|
-
|
|
267
|
+
const char *resolve_host = connect_addr_.empty() ? host_.c_str() : connect_addr_.c_str();
|
|
268
|
+
int rv = getaddrinfo(resolve_host, port_str, &hints, &res);
|
|
258
269
|
if (rv != 0) {
|
|
259
270
|
setError(gai_strerror(rv));
|
|
260
271
|
return -1;
|
|
@@ -266,12 +277,19 @@ class QuicClient {
|
|
|
266
277
|
if (fd == -1) {
|
|
267
278
|
continue;
|
|
268
279
|
}
|
|
269
|
-
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
|
|
280
|
+
if (::connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
|
|
270
281
|
memcpy(&remote_addr_, rp->ai_addr, rp->ai_addrlen);
|
|
271
282
|
remote_addrlen_ = (socklen_t)rp->ai_addrlen;
|
|
283
|
+
char buf[INET6_ADDRSTRLEN];
|
|
284
|
+
const void *src = (rp->ai_family == AF_INET)
|
|
285
|
+
? (void *)&((struct sockaddr_in *)rp->ai_addr)->sin_addr
|
|
286
|
+
: (void *)&((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr;
|
|
287
|
+
if (inet_ntop(rp->ai_family, src, buf, sizeof(buf))) {
|
|
288
|
+
resolved_address_ = buf;
|
|
289
|
+
}
|
|
272
290
|
break;
|
|
273
291
|
}
|
|
274
|
-
close(fd);
|
|
292
|
+
::close(fd);
|
|
275
293
|
fd = -1;
|
|
276
294
|
}
|
|
277
295
|
freeaddrinfo(res);
|
|
@@ -284,7 +302,7 @@ class QuicClient {
|
|
|
284
302
|
if (getsockname(fd, (struct sockaddr *)&local_addr_, &local_addrlen_) !=
|
|
285
303
|
0) {
|
|
286
304
|
setError("getsockname failed");
|
|
287
|
-
close(fd);
|
|
305
|
+
::close(fd);
|
|
288
306
|
return -1;
|
|
289
307
|
}
|
|
290
308
|
|
|
@@ -333,17 +351,21 @@ class QuicClient {
|
|
|
333
351
|
bool ca_loaded = false;
|
|
334
352
|
const char *ca_file = std::getenv("MQTT_QUIC_CA_FILE");
|
|
335
353
|
const char *ca_path = std::getenv("MQTT_QUIC_CA_PATH");
|
|
336
|
-
|
|
337
|
-
|
|
354
|
+
const char *file_arg = (ca_file && ca_file[0] != '\0') ? ca_file : nullptr;
|
|
355
|
+
const char *path_arg = (ca_path && ca_path[0] != '\0') ? ca_path : nullptr;
|
|
356
|
+
if (file_arg || path_arg) {
|
|
357
|
+
if (wolfSSL_CTX_load_verify_locations(ssl_ctx_, file_arg, path_arg) == 1) {
|
|
358
|
+
ca_loaded = true;
|
|
359
|
+
} else {
|
|
338
360
|
setError("Failed to load CA bundle from MQTT_QUIC_CA_FILE/CA_PATH");
|
|
339
361
|
return -1;
|
|
340
362
|
}
|
|
363
|
+
}
|
|
364
|
+
if (!ca_loaded && wolfSSL_CTX_set_default_verify_paths(ssl_ctx_) == 1) {
|
|
341
365
|
ca_loaded = true;
|
|
342
366
|
}
|
|
343
|
-
if (!ca_loaded) {
|
|
344
|
-
|
|
345
|
-
ca_loaded = true;
|
|
346
|
-
}
|
|
367
|
+
if (!ca_loaded && wolfSSL_CTX_load_system_CA_certs(ssl_ctx_) == 1) {
|
|
368
|
+
ca_loaded = true;
|
|
347
369
|
}
|
|
348
370
|
if (!ca_loaded) {
|
|
349
371
|
setError("No CA bundle available for TLS verification");
|
|
@@ -647,30 +669,44 @@ class QuicClient {
|
|
|
647
669
|
}
|
|
648
670
|
|
|
649
671
|
void cleanup() {
|
|
650
|
-
|
|
651
|
-
|
|
672
|
+
ngtcp2_conn *conn_to_del = nullptr;
|
|
673
|
+
void *ssl_to_free = nullptr;
|
|
674
|
+
void *ssl_ctx_to_free = nullptr;
|
|
675
|
+
int fd_to_close = -1;
|
|
676
|
+
int wake0 = -1, wake1 = -1;
|
|
677
|
+
{
|
|
678
|
+
std::lock_guard<std::mutex> lock(cleanup_mutex_);
|
|
679
|
+
conn_to_del = conn_;
|
|
652
680
|
conn_ = nullptr;
|
|
653
|
-
|
|
654
|
-
if (ssl_) {
|
|
655
|
-
wolfSSL_free(ssl_);
|
|
681
|
+
ssl_to_free = ssl_;
|
|
656
682
|
ssl_ = nullptr;
|
|
657
|
-
|
|
658
|
-
if (ssl_ctx_) {
|
|
659
|
-
wolfSSL_CTX_free(ssl_ctx_);
|
|
683
|
+
ssl_ctx_to_free = ssl_ctx_;
|
|
660
684
|
ssl_ctx_ = nullptr;
|
|
661
|
-
|
|
662
|
-
if (fd_ != -1) {
|
|
663
|
-
close(fd_);
|
|
685
|
+
fd_to_close = fd_;
|
|
664
686
|
fd_ = -1;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
close(wakeup_fds_[0]);
|
|
687
|
+
wake0 = wakeup_fds_[0];
|
|
688
|
+
wake1 = wakeup_fds_[1];
|
|
668
689
|
wakeup_fds_[0] = -1;
|
|
669
|
-
}
|
|
670
|
-
if (wakeup_fds_[1] != -1) {
|
|
671
|
-
close(wakeup_fds_[1]);
|
|
672
690
|
wakeup_fds_[1] = -1;
|
|
673
691
|
}
|
|
692
|
+
if (conn_to_del) {
|
|
693
|
+
ngtcp2_conn_del(conn_to_del);
|
|
694
|
+
}
|
|
695
|
+
if (ssl_to_free) {
|
|
696
|
+
wolfSSL_free(static_cast<WOLFSSL *>(ssl_to_free));
|
|
697
|
+
}
|
|
698
|
+
if (ssl_ctx_to_free) {
|
|
699
|
+
wolfSSL_CTX_free(static_cast<WOLFSSL_CTX *>(ssl_ctx_to_free));
|
|
700
|
+
}
|
|
701
|
+
if (fd_to_close != -1) {
|
|
702
|
+
::close(fd_to_close);
|
|
703
|
+
}
|
|
704
|
+
if (wake0 != -1) {
|
|
705
|
+
::close(wake0);
|
|
706
|
+
}
|
|
707
|
+
if (wake1 != -1) {
|
|
708
|
+
::close(wake1);
|
|
709
|
+
}
|
|
674
710
|
}
|
|
675
711
|
|
|
676
712
|
void clearError() {
|
|
@@ -740,10 +776,11 @@ class QuicClient {
|
|
|
740
776
|
return 0;
|
|
741
777
|
}
|
|
742
778
|
|
|
743
|
-
static int stream_close_cb(ngtcp2_conn *conn,
|
|
744
|
-
|
|
745
|
-
void *stream_user_data) {
|
|
779
|
+
static int stream_close_cb(ngtcp2_conn *conn, uint32_t flags,
|
|
780
|
+
int64_t stream_id, uint64_t app_error_code,
|
|
781
|
+
void *user_data, void *stream_user_data) {
|
|
746
782
|
(void)conn;
|
|
783
|
+
(void)flags;
|
|
747
784
|
(void)app_error_code;
|
|
748
785
|
(void)stream_user_data;
|
|
749
786
|
auto *client = static_cast<QuicClient *>(user_data);
|
|
@@ -761,9 +798,14 @@ class QuicClient {
|
|
|
761
798
|
return client->on_handshake_completed();
|
|
762
799
|
}
|
|
763
800
|
|
|
801
|
+
public:
|
|
802
|
+
const std::string &resolved_address() const { return resolved_address_; }
|
|
803
|
+
|
|
764
804
|
private:
|
|
765
805
|
std::string host_;
|
|
806
|
+
std::string connect_addr_;
|
|
766
807
|
uint16_t port_;
|
|
808
|
+
std::string resolved_address_;
|
|
767
809
|
|
|
768
810
|
int fd_;
|
|
769
811
|
struct sockaddr_storage remote_addr_;
|
|
@@ -795,6 +837,8 @@ class QuicClient {
|
|
|
795
837
|
|
|
796
838
|
mutable std::mutex err_mutex_;
|
|
797
839
|
std::string last_error_str_;
|
|
840
|
+
|
|
841
|
+
std::mutex cleanup_mutex_;
|
|
798
842
|
};
|
|
799
843
|
|
|
800
844
|
static std::map<jlong, std::unique_ptr<QuicClient>> connections;
|
|
@@ -822,6 +866,27 @@ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeCreateConnection(
|
|
|
822
866
|
return handle;
|
|
823
867
|
}
|
|
824
868
|
|
|
869
|
+
JNIEXPORT jlong JNICALL
|
|
870
|
+
Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeCreateConnectionWithAddress(
|
|
871
|
+
JNIEnv *env, jobject thiz, jstring hostnameForTls, jstring connectAddress, jint port) {
|
|
872
|
+
const char *tls_str = env->GetStringUTFChars(hostnameForTls, nullptr);
|
|
873
|
+
const char *addr_str = env->GetStringUTFChars(connectAddress, nullptr);
|
|
874
|
+
if (!tls_str || !addr_str) {
|
|
875
|
+
if (tls_str) env->ReleaseStringUTFChars(hostnameForTls, tls_str);
|
|
876
|
+
return 0;
|
|
877
|
+
}
|
|
878
|
+
std::string host_for_tls(tls_str);
|
|
879
|
+
std::string connect_addr(addr_str);
|
|
880
|
+
env->ReleaseStringUTFChars(hostnameForTls, tls_str);
|
|
881
|
+
env->ReleaseStringUTFChars(connectAddress, addr_str);
|
|
882
|
+
|
|
883
|
+
auto client = std::make_unique<QuicClient>(host_for_tls, connect_addr, (uint16_t)port);
|
|
884
|
+
std::lock_guard<std::mutex> lock(connections_mutex);
|
|
885
|
+
jlong handle = next_handle++;
|
|
886
|
+
connections[handle] = std::move(client);
|
|
887
|
+
return handle;
|
|
888
|
+
}
|
|
889
|
+
|
|
825
890
|
JNIEXPORT jint JNICALL
|
|
826
891
|
Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeConnect(
|
|
827
892
|
JNIEnv *env, jobject thiz, jlong connHandle) {
|
|
@@ -926,4 +991,26 @@ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError(
|
|
|
926
991
|
return env->NewStringUTF(it->second->last_error());
|
|
927
992
|
}
|
|
928
993
|
|
|
994
|
+
JNIEXPORT jstring JNICALL
|
|
995
|
+
Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastResolvedAddress(
|
|
996
|
+
JNIEnv *env, jobject thiz, jlong connHandle) {
|
|
997
|
+
std::lock_guard<std::mutex> lock(connections_mutex);
|
|
998
|
+
auto it = connections.find(connHandle);
|
|
999
|
+
if (it == connections.end()) {
|
|
1000
|
+
return nullptr;
|
|
1001
|
+
}
|
|
1002
|
+
const std::string &addr = it->second->resolved_address();
|
|
1003
|
+
if (addr.empty()) {
|
|
1004
|
+
return nullptr;
|
|
1005
|
+
}
|
|
1006
|
+
return env->NewStringUTF(addr.c_str());
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Debug-build alias: Kotlin/AGP can mangle the method name to include the module suffix.
|
|
1010
|
+
JNIEXPORT jstring JNICALL
|
|
1011
|
+
Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError_00024annadata_1capacitor_1mqtt_1quic_1debug__J(
|
|
1012
|
+
JNIEnv *env, jobject thiz, jlong connHandle) {
|
|
1013
|
+
return Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError(env, thiz, connHandle);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
929
1016
|
} // extern "C"
|
|
@@ -13,10 +13,13 @@ import android.system.Os
|
|
|
13
13
|
import android.util.Base64
|
|
14
14
|
import kotlinx.coroutines.CoroutineScope
|
|
15
15
|
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.delay
|
|
16
17
|
import kotlinx.coroutines.launch
|
|
18
|
+
import kotlinx.coroutines.withContext
|
|
17
19
|
import java.nio.charset.StandardCharsets
|
|
18
20
|
import java.io.File
|
|
19
21
|
import java.io.IOException
|
|
22
|
+
import java.net.InetAddress
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Capacitor plugin bridge. Phase 3: connect/publish/subscribe call into MQTTClient.
|
|
@@ -27,6 +30,29 @@ class MqttQuicPlugin : Plugin() {
|
|
|
27
30
|
private var client = MQTTClient(MQTTClient.ProtocolVersion.AUTO)
|
|
28
31
|
private val scope = CoroutineScope(Dispatchers.Main)
|
|
29
32
|
|
|
33
|
+
/** Last resolved IP per host (used when DNS fails on reconnect). */
|
|
34
|
+
@Volatile
|
|
35
|
+
private var lastResolvedHost: String? = null
|
|
36
|
+
@Volatile
|
|
37
|
+
private var lastResolvedIp: String? = null
|
|
38
|
+
|
|
39
|
+
/** Resolve hostname to IP for socket connect (avoids "No address" on reconnect). Returns null if resolution fails. */
|
|
40
|
+
private fun resolveHostToIp(host: String): String? {
|
|
41
|
+
return try {
|
|
42
|
+
InetAddress.getAllByName(host).firstOrNull()?.hostAddress?.also { ip ->
|
|
43
|
+
lastResolvedHost = host
|
|
44
|
+
lastResolvedIp = ip
|
|
45
|
+
}
|
|
46
|
+
} catch (e: Exception) {
|
|
47
|
+
null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Use cached IP for this host if fresh resolve failed (e.g. on reconnect). */
|
|
52
|
+
private fun resolveOrCachedIp(host: String): String? {
|
|
53
|
+
return resolveHostToIp(host) ?: if (host == lastResolvedHost) lastResolvedIp else null
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
private fun bundledCaFilePath(): String? {
|
|
31
57
|
return try {
|
|
32
58
|
val assetName = "mqttquic_ca.pem"
|
|
@@ -100,9 +126,38 @@ class MqttQuicPlugin : Plugin() {
|
|
|
100
126
|
notifyListeners("message", data)
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
// Resolve host to IP on IO so native getaddrinfo gets an IP (avoids "No address associated with hostname" on reconnect)
|
|
130
|
+
val noAddressMsg = "No address associated with hostname"
|
|
131
|
+
var lastException: Exception? = null
|
|
132
|
+
for (attempt in 1..2) {
|
|
133
|
+
try {
|
|
134
|
+
withContext(Dispatchers.IO) {
|
|
135
|
+
val resolvedIp = resolveOrCachedIp(host)
|
|
136
|
+
client.connect(host, port, clientId, username, password, cleanSession ?: true, keepalive ?: 20, sessionExpiryInterval, connectAddress = resolvedIp)
|
|
137
|
+
}
|
|
138
|
+
// Cache resolved IP from native so reconnect can use it when Java DNS fails
|
|
139
|
+
client.getLastResolvedAddress()?.let { ip ->
|
|
140
|
+
lastResolvedHost = host
|
|
141
|
+
lastResolvedIp = ip
|
|
142
|
+
}
|
|
143
|
+
call.resolve(JSObject().put("connected", true))
|
|
144
|
+
notifyListeners("connected", JSObject().put("connected", true))
|
|
145
|
+
return@launch
|
|
146
|
+
} catch (e: Exception) {
|
|
147
|
+
// Cache resolved IP from native even on failure (e.g. CONNACK timeout) so reconnect can use it
|
|
148
|
+
client.getLastResolvedAddress()?.let { ip ->
|
|
149
|
+
lastResolvedHost = host
|
|
150
|
+
lastResolvedIp = ip
|
|
151
|
+
}
|
|
152
|
+
lastException = e
|
|
153
|
+
if (attempt == 1 && e.message?.contains(noAddressMsg, ignoreCase = true) == true) {
|
|
154
|
+
delay(2000L)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
call.reject(lastException?.message ?: "Connection failed")
|
|
106
161
|
} catch (e: Exception) {
|
|
107
162
|
call.reject(e.message ?: "Connection failed")
|
|
108
163
|
}
|
|
@@ -199,7 +254,7 @@ class MqttQuicPlugin : Plugin() {
|
|
|
199
254
|
val properties = mutableMapOf<Int, Any>()
|
|
200
255
|
messageExpiryInterval?.let { properties[MQTT5PropertyType.MESSAGE_EXPIRY_INTERVAL.toInt()] = it }
|
|
201
256
|
contentType?.let { properties[MQTT5PropertyType.CONTENT_TYPE.toInt()] = it }
|
|
202
|
-
client.publish(topic, data, minOf(qos, 2), if (properties.isNotEmpty()) properties else null)
|
|
257
|
+
client.publish(topic, data, minOf(qos ?: 0, 2), if (properties.isNotEmpty()) properties else null)
|
|
203
258
|
call.resolve(JSObject().put("success", true))
|
|
204
259
|
} catch (e: Exception) {
|
|
205
260
|
val msg = e.message ?: "Publish failed"
|
|
@@ -226,7 +281,7 @@ class MqttQuicPlugin : Plugin() {
|
|
|
226
281
|
|
|
227
282
|
scope.launch {
|
|
228
283
|
try {
|
|
229
|
-
client.subscribe(topic, minOf(qos, 2), subscriptionIdentifier)
|
|
284
|
+
client.subscribe(topic, minOf(qos ?: 0, 2), subscriptionIdentifier)
|
|
230
285
|
call.resolve(JSObject().put("success", true))
|
|
231
286
|
} catch (e: Exception) {
|
|
232
287
|
call.reject(e.message ?: "Subscribe failed")
|