@annadata/capacitor-mqtt-quic 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/README.md +399 -0
- package/android/NGTCP2_BUILD_INSTRUCTIONS.md +319 -0
- package/android/build-nghttp3.sh +182 -0
- package/android/build-ngtcp2.sh +289 -0
- package/android/build-openssl.sh +302 -0
- package/android/build.gradle +75 -0
- package/android/gradle.properties +3 -0
- package/android/proguard-rules.pro +2 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/assets/mqttquic_ca.pem +5 -0
- package/android/src/main/cpp/CMakeLists.txt +157 -0
- package/android/src/main/cpp/ngtcp2_jni.cpp +928 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +232 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +339 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5Properties.kt +250 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5Protocol.kt +281 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5ReasonCodes.kt +109 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocol.kt +249 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTTypes.kt +47 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/NGTCP2Client.kt +184 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicClientStub.kt +54 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicTypes.kt +21 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/transport/QUICStreamAdapter.kt +37 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/transport/StreamTransport.kt +91 -0
- package/android/src/test/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocolTest.kt +92 -0
- package/dist/esm/definitions.d.ts +66 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +28 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +183 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +217 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +215 -0
- package/dist/plugin.js.map +1 -0
- package/ios/MqttQuicPlugin.podspec +34 -0
- package/ios/NGTCP2_BUILD_INSTRUCTIONS.md +302 -0
- package/ios/Sources/MqttQuicPlugin/Client/MQTTClient.swift +343 -0
- package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5Properties.swift +280 -0
- package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5Protocol.swift +333 -0
- package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5ReasonCodes.swift +113 -0
- package/ios/Sources/MqttQuicPlugin/MQTT/MQTTProtocol.swift +322 -0
- package/ios/Sources/MqttQuicPlugin/MQTT/MQTTTypes.swift +54 -0
- package/ios/Sources/MqttQuicPlugin/MqttQuicPlugin.swift +229 -0
- package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Bridge.h +29 -0
- package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Bridge.mm +865 -0
- package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Client.swift +262 -0
- package/ios/Sources/MqttQuicPlugin/QUIC/QuicClientStub.swift +66 -0
- package/ios/Sources/MqttQuicPlugin/QUIC/QuicTypes.swift +23 -0
- package/ios/Sources/MqttQuicPlugin/Resources/mqttquic_ca.pem +5 -0
- package/ios/Sources/MqttQuicPlugin/Transport/QUICStreamAdapter.swift +50 -0
- package/ios/Sources/MqttQuicPlugin/Transport/StreamTransport.swift +105 -0
- package/ios/Tests/MQTTProtocolTests.swift +82 -0
- package/ios/build-nghttp3.sh +173 -0
- package/ios/build-ngtcp2.sh +278 -0
- package/ios/build-openssl.sh +405 -0
- package/package.json +63 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'MqttQuicPlugin'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = 'MQTT-over-QUIC Capacitor plugin (iOS)'
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = 'https://github.com/annadata/capacitor-mqtt-quic'
|
|
11
|
+
s.author = 'Annadata'
|
|
12
|
+
s.source = { :git => 'https://github.com/annadata/capacitor-mqtt-quic', :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.resources = ['Sources/MqttQuicPlugin/Resources/*.pem']
|
|
15
|
+
s.ios.deployment_target = '15.0'
|
|
16
|
+
s.dependency 'Capacitor'
|
|
17
|
+
s.swift_version = '5.1'
|
|
18
|
+
s.vendored_libraries = [
|
|
19
|
+
'libs/libngtcp2.a',
|
|
20
|
+
'libs/libngtcp2_crypto_quictls.a',
|
|
21
|
+
'libs/libnghttp3.a',
|
|
22
|
+
'libs/libssl.a',
|
|
23
|
+
'libs/libcrypto.a'
|
|
24
|
+
]
|
|
25
|
+
s.public_header_files = 'Sources/**/*.h', 'include/**/*.h'
|
|
26
|
+
s.private_header_files = 'Sources/**/*.h', 'include/**/*.h'
|
|
27
|
+
s.header_mappings_dir = 'Sources'
|
|
28
|
+
s.pod_target_xcconfig = {
|
|
29
|
+
'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/MqttQuicPlugin/include/ngtcp2 $(PODS_ROOT)/MqttQuicPlugin/include/nghttp3 $(PODS_ROOT)/MqttQuicPlugin/include/openssl',
|
|
30
|
+
'LIBRARY_SEARCH_PATHS' => '$(PODS_ROOT)/MqttQuicPlugin/libs',
|
|
31
|
+
'OTHER_LDFLAGS' => '-lngtcp2 -lngtcp2_crypto_quictls -lnghttp3 -lssl -lcrypto',
|
|
32
|
+
'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'NGTCP2_ENABLED NGHTTP3_ENABLED'
|
|
33
|
+
}
|
|
34
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Building ngtcp2 for iOS
|
|
2
|
+
|
|
3
|
+
This document provides instructions for building ngtcp2 and OpenSSL for iOS to enable real QUIC transport in the MQTT client.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Xcode 14+** (for iOS 15+)
|
|
8
|
+
- **CMake 3.20+**
|
|
9
|
+
- **iOS SDK 15.0+**
|
|
10
|
+
- **Git** (for cloning repositories)
|
|
11
|
+
|
|
12
|
+
**Source layout:** set `PROJECT_DIR` to the `capacitor-mqtt-quic` repo root.
|
|
13
|
+
Build scripts then expect dependencies under:
|
|
14
|
+
- `$PROJECT_DIR/ref-code/ngtcp2`
|
|
15
|
+
- `$PROJECT_DIR/ref-code/nghttp3`
|
|
16
|
+
- `$PROJECT_DIR/ref-code/openssl` (or `$PROJECT_DIR/ref-code.openssl`)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export PROJECT_DIR="/Users/annadata/Project_A/annadata-production/ref-code/capacitor-mqtt-quic"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Override with `NGTCP2_SOURCE_DIR`, `NGHTTP3_SOURCE_DIR`, `OPENSSL_SOURCE_DIR`
|
|
23
|
+
if you store sources elsewhere.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Option 1: Use Pre-built Libraries (Recommended for Development)
|
|
28
|
+
|
|
29
|
+
If you have access to pre-built ngtcp2, nghttp3, and OpenSSL libraries:
|
|
30
|
+
|
|
31
|
+
1. Place `libngtcp2.a` in `ios/libs/`
|
|
32
|
+
2. Place `libnghttp3.a` in `ios/libs/`
|
|
33
|
+
3. Place OpenSSL libraries (`libssl.a`, `libcrypto.a`) in `ios/libs/`
|
|
34
|
+
4. Place headers in `ios/include/ngtcp2/`, `ios/include/nghttp3/`, and `ios/include/openssl/`
|
|
35
|
+
5. Update `MqttQuicPlugin.podspec` to link against these libraries
|
|
36
|
+
|
|
37
|
+
### Option 2: Build from Source
|
|
38
|
+
|
|
39
|
+
#### Step 1: Build OpenSSL for iOS (QUIC TLS)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd ios
|
|
43
|
+
./build-openssl.sh --arch arm64 --sdk iphoneos --quictls
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This will:
|
|
47
|
+
- Clone quictls (OpenSSL fork with QUIC API) if not present
|
|
48
|
+
- Build static libraries for iOS
|
|
49
|
+
- Install to `ios/install/openssl-ios/`
|
|
50
|
+
- Sync `libssl.a`, `libcrypto.a` to `ios/libs/` and headers to `ios/include/openssl/`
|
|
51
|
+
|
|
52
|
+
**Note:** For simulator builds, use:
|
|
53
|
+
```bash
|
|
54
|
+
./build-openssl.sh --arch x86_64 --sdk iphonesimulator
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Step 2: Build nghttp3 for iOS
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd ios
|
|
61
|
+
./build-nghttp3.sh \
|
|
62
|
+
--arch arm64 \
|
|
63
|
+
--sdk iphoneos
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This will:
|
|
67
|
+
- Build nghttp3 static library
|
|
68
|
+
- Install to `ios/build/nghttp3-ios-arm64/install/`
|
|
69
|
+
- Sync `libnghttp3.a` to `ios/libs/` and headers to `ios/include/nghttp3/`
|
|
70
|
+
|
|
71
|
+
#### Step 3: Build ngtcp2 for iOS
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd ios
|
|
75
|
+
./build-ngtcp2.sh \
|
|
76
|
+
--openssl-path ./install/openssl-ios \
|
|
77
|
+
--arch arm64 \
|
|
78
|
+
--sdk iphoneos \
|
|
79
|
+
--quictls
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This will:
|
|
83
|
+
- Build ngtcp2 static library
|
|
84
|
+
- Link against OpenSSL (quictls)
|
|
85
|
+
- Install to `ios/build/ios-arm64/install/`
|
|
86
|
+
- Sync `libngtcp2.a` and `libngtcp2_crypto_quictls.a` to `ios/libs/` and headers to `ios/include/ngtcp2/`
|
|
87
|
+
|
|
88
|
+
#### Step 4: Build for Multiple Architectures
|
|
89
|
+
|
|
90
|
+
For a universal library (arm64 + x86_64 for simulator):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Build for device (arm64)
|
|
94
|
+
./build-openssl.sh --arch arm64 --sdk iphoneos --quictls
|
|
95
|
+
./build-nghttp3.sh --arch arm64 --sdk iphoneos
|
|
96
|
+
./build-ngtcp2.sh --openssl-path ./install/openssl-ios --arch arm64 --sdk iphoneos --quictls
|
|
97
|
+
|
|
98
|
+
# Build for simulator (x86_64)
|
|
99
|
+
./build-openssl.sh --arch x86_64 --sdk iphonesimulator --quictls
|
|
100
|
+
./build-nghttp3.sh --arch x86_64 --sdk iphonesimulator
|
|
101
|
+
./build-ngtcp2.sh --openssl-path ./install/openssl-ios --arch x86_64 --sdk iphonesimulator --quictls
|
|
102
|
+
|
|
103
|
+
# Create universal library
|
|
104
|
+
lipo -create \
|
|
105
|
+
build/ios-arm64/install/lib/libngtcp2.a \
|
|
106
|
+
build/ios-x86_64/install/lib/libngtcp2.a \
|
|
107
|
+
-output libs/libngtcp2-universal.a
|
|
108
|
+
|
|
109
|
+
# Create universal nghttp3 library
|
|
110
|
+
lipo -create \
|
|
111
|
+
build/nghttp3-ios-arm64/install/lib/libnghttp3.a \
|
|
112
|
+
build/nghttp3-ios-x86_64/install/lib/libnghttp3.a \
|
|
113
|
+
-output libs/libnghttp3-universal.a
|
|
114
|
+
|
|
115
|
+
# Create universal ngtcp2_crypto_quictls library
|
|
116
|
+
lipo -create \
|
|
117
|
+
build/ios-arm64/install/lib/libngtcp2_crypto_quictls.a \
|
|
118
|
+
build/ios-x86_64/install/lib/libngtcp2_crypto_quictls.a \
|
|
119
|
+
-output libs/libngtcp2_crypto_quictls-universal.a
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Integration into Xcode Project
|
|
123
|
+
|
|
124
|
+
### Option A: CocoaPods (Recommended)
|
|
125
|
+
|
|
126
|
+
Update `ios/MqttQuicPlugin.podspec`:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
Pod::Spec.new do |s|
|
|
130
|
+
# ... existing configuration ...
|
|
131
|
+
|
|
132
|
+
# ngtcp2 static library
|
|
133
|
+
s.vendored_libraries = 'libs/libngtcp2.a', 'libs/libngtcp2_crypto_quictls.a', 'libs/libnghttp3.a', 'libs/libssl.a', 'libs/libcrypto.a'
|
|
134
|
+
|
|
135
|
+
# Header search paths
|
|
136
|
+
s.public_header_files = 'Sources/**/*.h'
|
|
137
|
+
s.private_header_files = 'Sources/**/*.h'
|
|
138
|
+
s.header_mappings_dir = 'Sources'
|
|
139
|
+
|
|
140
|
+
# Include ngtcp2/nghttp3 headers
|
|
141
|
+
s.xcconfig = {
|
|
142
|
+
'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/MqttQuicPlugin/include/ngtcp2 $(PODS_ROOT)/MqttQuicPlugin/include/nghttp3 $(PODS_ROOT)/MqttQuicPlugin/include/openssl',
|
|
143
|
+
'LIBRARY_SEARCH_PATHS' => '$(PODS_ROOT)/MqttQuicPlugin/libs',
|
|
144
|
+
'OTHER_LDFLAGS' => '-lngtcp2 -lngtcp2_crypto_quictls -lnghttp3 -lssl -lcrypto'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# OpenSSL dependency (if using CocoaPods)
|
|
148
|
+
s.dependency 'OpenSSL-Universal', '~> 3.0'
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Option B: Manual Integration
|
|
153
|
+
|
|
154
|
+
1. Add `libngtcp2.a` and `libnghttp3.a` to Xcode project
|
|
155
|
+
2. Add header search paths:
|
|
156
|
+
- `$(SRCROOT)/../ios/include/ngtcp2`
|
|
157
|
+
- `$(SRCROOT)/../ios/include/nghttp3`
|
|
158
|
+
- `$(SRCROOT)/../ios/include/openssl`
|
|
159
|
+
3. Link against:
|
|
160
|
+
- `libngtcp2.a`
|
|
161
|
+
- `libnghttp3.a`
|
|
162
|
+
- `libssl.a` (OpenSSL)
|
|
163
|
+
- `libcrypto.a` (OpenSSL)
|
|
164
|
+
|
|
165
|
+
## TLS Certificate Verification (QUIC)
|
|
166
|
+
|
|
167
|
+
QUIC requires TLS 1.3 and certificate verification is **enabled by default**.
|
|
168
|
+
You can bundle a CA PEM and it will be loaded automatically:
|
|
169
|
+
|
|
170
|
+
- `ios/Sources/MqttQuicPlugin/Resources/mqttquic_ca.pem`
|
|
171
|
+
|
|
172
|
+
You can also override per call:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
await MqttQuic.connect({
|
|
176
|
+
host: 'mqtt.example.com',
|
|
177
|
+
port: 1884,
|
|
178
|
+
clientId: 'my-client-id',
|
|
179
|
+
caFile: '/path/to/ca-bundle.pem',
|
|
180
|
+
// or caPath: '/path/to/ca-directory'
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### How to generate certificates
|
|
185
|
+
|
|
186
|
+
**Option A: Public CA (Let’s Encrypt)**
|
|
187
|
+
You do not bundle `mqttquic_ca.pem` (the OS already trusts public CAs).
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
sudo apt-get update
|
|
191
|
+
sudo apt-get install -y certbot
|
|
192
|
+
sudo certbot certonly --standalone -d mqtt.example.com
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use on server:
|
|
196
|
+
- Cert: `/etc/letsencrypt/live/mqtt.example.com/fullchain.pem`
|
|
197
|
+
- Key: `/etc/letsencrypt/live/mqtt.example.com/privkey.pem`
|
|
198
|
+
|
|
199
|
+
**Option B: Private CA (dev/internal)**
|
|
200
|
+
Generate your own CA, sign the server cert, and bundle the CA PEM.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
mkdir -p certs && cd certs
|
|
204
|
+
|
|
205
|
+
# 1) Create CA (one-time)
|
|
206
|
+
openssl genrsa -out ca.key 4096
|
|
207
|
+
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem \
|
|
208
|
+
-subj "/C=US/ST=CA/L=SF/O=Annadata/OU=MQTT/CN=Annadata-Root-CA"
|
|
209
|
+
|
|
210
|
+
# 2) Create server key + CSR
|
|
211
|
+
openssl genrsa -out server.key 2048
|
|
212
|
+
openssl req -new -key server.key -out server.csr \
|
|
213
|
+
-subj "/C=US/ST=CA/L=SF/O=Annadata/OU=MQTT/CN=mqtt.example.com"
|
|
214
|
+
|
|
215
|
+
# 3) Add SANs (edit DNS/IP)
|
|
216
|
+
cat > server_ext.cnf <<EOF
|
|
217
|
+
subjectAltName = DNS:mqtt.example.com,IP:YOUR.SERVER.IP
|
|
218
|
+
extendedKeyUsage = serverAuth
|
|
219
|
+
EOF
|
|
220
|
+
|
|
221
|
+
# 4) Sign server cert
|
|
222
|
+
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
|
|
223
|
+
-out server.pem -days 365 -sha256 -extfile server_ext.cnf
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Bundle the CA cert (never ship `ca.key`):
|
|
227
|
+
- iOS: `ios/Sources/MqttQuicPlugin/Resources/mqttquic_ca.pem` (use `ca.pem`)
|
|
228
|
+
|
|
229
|
+
## Test Harness (QUIC Smoke Test)
|
|
230
|
+
|
|
231
|
+
This runs: connect → subscribe → publish → disconnect.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
await MqttQuic.testHarness({
|
|
235
|
+
host: 'mqtt.example.com',
|
|
236
|
+
port: 1884,
|
|
237
|
+
clientId: 'mqttquic_test_client',
|
|
238
|
+
topic: 'test/topic',
|
|
239
|
+
payload: 'Hello QUIC!',
|
|
240
|
+
// optional CA override
|
|
241
|
+
caFile: '/path/to/ca-bundle.pem'
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Using Pre-built Libraries
|
|
246
|
+
|
|
247
|
+
If you prefer to use pre-built libraries:
|
|
248
|
+
|
|
249
|
+
### OpenSSL
|
|
250
|
+
|
|
251
|
+
Download from:
|
|
252
|
+
- https://github.com/x2on/OpenSSL-for-iPhone
|
|
253
|
+
- https://github.com/krzyzanowskim/OpenSSL
|
|
254
|
+
|
|
255
|
+
### ngtcp2
|
|
256
|
+
|
|
257
|
+
Currently, there are no widely available pre-built ngtcp2 libraries for iOS. You'll need to build from source.
|
|
258
|
+
|
|
259
|
+
### nghttp3
|
|
260
|
+
|
|
261
|
+
Currently, there are no widely available pre-built nghttp3 libraries for iOS. You'll need to build from source. Make sure you clone with submodules:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone --recurse-submodules https://github.com/ngtcp2/nghttp3.git
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Troubleshooting
|
|
268
|
+
|
|
269
|
+
### CMake Not Found
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Install via Homebrew
|
|
273
|
+
brew install cmake
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### OpenSSL Build Fails
|
|
277
|
+
|
|
278
|
+
- Ensure you're using OpenSSL 3.0+ (required for TLS 1.3)
|
|
279
|
+
- Check that Xcode command line tools are installed: `xcode-select --install`
|
|
280
|
+
- Verify SDK path: `xcrun --sdk iphoneos --show-sdk-path`
|
|
281
|
+
|
|
282
|
+
### ngtcp2 Build Fails
|
|
283
|
+
|
|
284
|
+
- Verify OpenSSL is built and path is correct
|
|
285
|
+
- Check CMake version: `cmake --version` (must be 3.20+)
|
|
286
|
+
- Ensure iOS deployment target matches (15.0+)
|
|
287
|
+
|
|
288
|
+
### Link Errors
|
|
289
|
+
|
|
290
|
+
- Verify all libraries are built for the same architecture
|
|
291
|
+
- Check that header search paths are correct
|
|
292
|
+
- Ensure OpenSSL and ngtcp2 are linked in the correct order
|
|
293
|
+
|
|
294
|
+
## Next Steps
|
|
295
|
+
|
|
296
|
+
After building ngtcp2:
|
|
297
|
+
|
|
298
|
+
1. Update `NGTCP2Client.swift` to implement the TODO sections
|
|
299
|
+
2. Replace `QuicClientStub` with `NGTCP2Client` in `MQTTClient.swift`
|
|
300
|
+
3. Test connection to MQTT server over QUIC
|
|
301
|
+
|
|
302
|
+
See `NGTCP2_INTEGRATION_PLAN.md` for detailed implementation guide.
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MQTTClient.swift
|
|
3
|
+
// MqttQuicPlugin
|
|
4
|
+
//
|
|
5
|
+
// High-level MQTT client: connect, publish, subscribe, disconnect.
|
|
6
|
+
// Uses QuicClient + stream adapters + MQTT protocol.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
public final class MQTTClient {
|
|
12
|
+
|
|
13
|
+
public enum State {
|
|
14
|
+
case disconnected
|
|
15
|
+
case connecting
|
|
16
|
+
case connected
|
|
17
|
+
case error(String)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public enum ProtocolVersion {
|
|
21
|
+
case v311
|
|
22
|
+
case v5
|
|
23
|
+
case auto // Try 5.0 first, fallback to 3.1.1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private var state: State = .disconnected
|
|
27
|
+
private var protocolVersion: ProtocolVersion = .auto
|
|
28
|
+
private var activeProtocolVersion: UInt8 = 0 // 0x04 or 0x05
|
|
29
|
+
private var quicClient: QuicClientProtocol?
|
|
30
|
+
private var stream: QuicStreamProtocol?
|
|
31
|
+
private var reader: MQTTStreamReaderProtocol?
|
|
32
|
+
private var writer: MQTTStreamWriterProtocol?
|
|
33
|
+
private var messageLoopTask: Task<Void, Error>?
|
|
34
|
+
private var nextPacketId: UInt16 = 1
|
|
35
|
+
private var subscribedTopics: [String: (Data) -> Void] = [:]
|
|
36
|
+
private let lock = NSLock()
|
|
37
|
+
|
|
38
|
+
public init(protocolVersion: ProtocolVersion = .auto) {
|
|
39
|
+
self.protocolVersion = protocolVersion
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func getState() -> State {
|
|
43
|
+
lock.lock()
|
|
44
|
+
defer { lock.unlock() }
|
|
45
|
+
return state
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func connect(host: String, port: UInt16, clientId: String, username: String?, password: String?, cleanSession: Bool, keepalive: UInt16, sessionExpiryInterval: UInt32? = nil) async throws {
|
|
49
|
+
lock.lock()
|
|
50
|
+
if case .connecting = state {
|
|
51
|
+
lock.unlock()
|
|
52
|
+
throw MQTTProtocolError.insufficientData("already connecting")
|
|
53
|
+
}
|
|
54
|
+
state = .connecting
|
|
55
|
+
lock.unlock()
|
|
56
|
+
|
|
57
|
+
do {
|
|
58
|
+
// Determine protocol version
|
|
59
|
+
let useV5 = protocolVersion == .v5 || (protocolVersion == .auto)
|
|
60
|
+
|
|
61
|
+
let quic: QuicClientProtocol
|
|
62
|
+
#if NGTCP2_ENABLED
|
|
63
|
+
quic = NGTCP2Client()
|
|
64
|
+
#else
|
|
65
|
+
// Build CONNACK stub (used when ngtcp2 is not linked)
|
|
66
|
+
let connack: Data
|
|
67
|
+
if useV5 {
|
|
68
|
+
connack = try MQTT5Protocol.buildConnackV5(reasonCode: .success, sessionPresent: false)
|
|
69
|
+
} else {
|
|
70
|
+
connack = MQTTProtocol.buildConnack(returnCode: MQTTConnAckCode.accepted.rawValue)
|
|
71
|
+
}
|
|
72
|
+
quic = QuicClientStub(initialReadData: connack)
|
|
73
|
+
#endif
|
|
74
|
+
try await quic.connect(host: host, port: port)
|
|
75
|
+
let s = try await quic.openStream()
|
|
76
|
+
let r = QUICStreamReader(stream: s)
|
|
77
|
+
let w = QUICStreamWriter(stream: s)
|
|
78
|
+
|
|
79
|
+
lock.lock()
|
|
80
|
+
quicClient = quic
|
|
81
|
+
stream = s
|
|
82
|
+
reader = r
|
|
83
|
+
writer = w
|
|
84
|
+
lock.unlock()
|
|
85
|
+
|
|
86
|
+
// Build CONNECT
|
|
87
|
+
let connectData: Data
|
|
88
|
+
if useV5 {
|
|
89
|
+
connectData = try MQTT5Protocol.buildConnectV5(
|
|
90
|
+
clientId: clientId,
|
|
91
|
+
username: username,
|
|
92
|
+
password: password,
|
|
93
|
+
keepalive: keepalive,
|
|
94
|
+
cleanStart: cleanSession,
|
|
95
|
+
sessionExpiryInterval: sessionExpiryInterval
|
|
96
|
+
)
|
|
97
|
+
activeProtocolVersion = MQTTProtocolLevel.v5
|
|
98
|
+
} else {
|
|
99
|
+
connectData = try MQTTProtocol.buildConnect(
|
|
100
|
+
clientId: clientId,
|
|
101
|
+
username: username,
|
|
102
|
+
password: password,
|
|
103
|
+
keepalive: keepalive,
|
|
104
|
+
cleanSession: cleanSession
|
|
105
|
+
)
|
|
106
|
+
activeProtocolVersion = MQTTProtocolLevel.v311
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try await w.write(connectData)
|
|
110
|
+
try await w.drain()
|
|
111
|
+
|
|
112
|
+
// Read CONNACK
|
|
113
|
+
let fixed = try await r.readexactly(2)
|
|
114
|
+
let (msgType, remLen, hdrLen) = try MQTTProtocol.parseFixedHeader(Data(fixed))
|
|
115
|
+
let rest = try await r.readexactly(remLen)
|
|
116
|
+
var full = Data(fixed)
|
|
117
|
+
full.append(rest)
|
|
118
|
+
|
|
119
|
+
if msgType != MQTTMessageType.CONNACK.rawValue {
|
|
120
|
+
lock.lock()
|
|
121
|
+
state = .error("expected CONNACK, got \(msgType)")
|
|
122
|
+
lock.unlock()
|
|
123
|
+
throw MQTTProtocolError.insufficientData("expected CONNACK")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse CONNACK based on protocol version
|
|
127
|
+
if activeProtocolVersion == MQTTProtocolLevel.v5 {
|
|
128
|
+
let (_, reasonCode, _, _) = try MQTT5Protocol.parseConnackV5(full, offset: hdrLen)
|
|
129
|
+
if reasonCode != .success {
|
|
130
|
+
lock.lock()
|
|
131
|
+
state = .error("CONNACK refused: \(reasonCode)")
|
|
132
|
+
lock.unlock()
|
|
133
|
+
throw MQTTProtocolError.insufficientData("CONNACK refused: \(reasonCode)")
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
let (_, returnCode) = try MQTTProtocol.parseConnack(full, offset: hdrLen)
|
|
137
|
+
if returnCode != MQTTConnAckCode.accepted.rawValue {
|
|
138
|
+
lock.lock()
|
|
139
|
+
state = .error("CONNACK refused: \(returnCode)")
|
|
140
|
+
lock.unlock()
|
|
141
|
+
throw MQTTProtocolError.insufficientData("CONNACK refused")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lock.lock()
|
|
146
|
+
state = .connected
|
|
147
|
+
lock.unlock()
|
|
148
|
+
|
|
149
|
+
startMessageLoop()
|
|
150
|
+
} catch {
|
|
151
|
+
lock.lock()
|
|
152
|
+
let w = writer
|
|
153
|
+
quicClient = nil
|
|
154
|
+
stream = nil
|
|
155
|
+
reader = nil
|
|
156
|
+
writer = nil
|
|
157
|
+
state = .error("\(error)")
|
|
158
|
+
lock.unlock()
|
|
159
|
+
try? await w?.close()
|
|
160
|
+
throw error
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public func publish(topic: String, payload: Data, qos: UInt8, properties: [UInt8: Any]? = nil) async throws {
|
|
165
|
+
guard case .connected = getState() else { throw MQTTProtocolError.insufficientData("not connected") }
|
|
166
|
+
lock.lock()
|
|
167
|
+
let w = writer
|
|
168
|
+
let version = activeProtocolVersion
|
|
169
|
+
lock.unlock()
|
|
170
|
+
guard let w = w else { throw MQTTProtocolError.insufficientData("no writer") }
|
|
171
|
+
|
|
172
|
+
let pid: UInt16? = qos > 0 ? nextPacketIdUsed() : nil
|
|
173
|
+
let data: Data
|
|
174
|
+
if version == MQTTProtocolLevel.v5 {
|
|
175
|
+
data = try MQTT5Protocol.buildPublishV5(topic: topic, payload: payload, packetId: pid, qos: qos, retain: false, properties: properties)
|
|
176
|
+
} else {
|
|
177
|
+
data = try MQTTProtocol.buildPublish(topic: topic, payload: payload, packetId: pid, qos: qos, retain: false)
|
|
178
|
+
}
|
|
179
|
+
try await w.write(data)
|
|
180
|
+
try await w.drain()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public func subscribe(topic: String, qos: UInt8, subscriptionIdentifier: Int? = nil) async throws {
|
|
184
|
+
guard case .connected = getState() else { throw MQTTProtocolError.insufficientData("not connected") }
|
|
185
|
+
lock.lock()
|
|
186
|
+
let r = reader, w = writer
|
|
187
|
+
let version = activeProtocolVersion
|
|
188
|
+
lock.unlock()
|
|
189
|
+
guard let r = r, let w = w else { throw MQTTProtocolError.insufficientData("no reader/writer") }
|
|
190
|
+
|
|
191
|
+
let pid = nextPacketIdUsed()
|
|
192
|
+
let data: Data
|
|
193
|
+
if version == MQTTProtocolLevel.v5 {
|
|
194
|
+
data = try MQTT5Protocol.buildSubscribeV5(packetId: pid, topic: topic, qos: qos, subscriptionIdentifier: subscriptionIdentifier)
|
|
195
|
+
} else {
|
|
196
|
+
data = try MQTTProtocol.buildSubscribe(packetId: pid, topic: topic, qos: qos)
|
|
197
|
+
}
|
|
198
|
+
try await w.write(data)
|
|
199
|
+
try await w.drain()
|
|
200
|
+
|
|
201
|
+
let fixed = try await r.readexactly(2)
|
|
202
|
+
let (_, remLen, hdrLen) = try MQTTProtocol.parseFixedHeader(Data(fixed))
|
|
203
|
+
let rest = try await r.readexactly(remLen)
|
|
204
|
+
var full = Data(fixed)
|
|
205
|
+
full.append(rest)
|
|
206
|
+
|
|
207
|
+
if version == MQTTProtocolLevel.v5 {
|
|
208
|
+
let (_, reasonCodes, _, _) = try MQTT5Protocol.parseSubackV5(full, offset: hdrLen)
|
|
209
|
+
if let firstRC = reasonCodes.first, firstRC != .grantedQoS0 && firstRC != .grantedQoS1 && firstRC != .grantedQoS2 {
|
|
210
|
+
throw MQTTProtocolError.insufficientData("SUBACK error \(firstRC)")
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
let (_, rc, _) = try MQTTProtocol.parseSuback(full, offset: hdrLen)
|
|
214
|
+
if rc > 0x02 { throw MQTTProtocolError.insufficientData("SUBACK error \(rc)") }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public func unsubscribe(topic: String) async throws {
|
|
219
|
+
guard case .connected = getState() else { throw MQTTProtocolError.insufficientData("not connected") }
|
|
220
|
+
lock.lock()
|
|
221
|
+
let r = reader, w = writer
|
|
222
|
+
let version = activeProtocolVersion
|
|
223
|
+
subscribedTopics.removeValue(forKey: topic)
|
|
224
|
+
lock.unlock()
|
|
225
|
+
guard let r = r, let w = w else { throw MQTTProtocolError.insufficientData("no reader/writer") }
|
|
226
|
+
|
|
227
|
+
let pid = nextPacketIdUsed()
|
|
228
|
+
let data: Data
|
|
229
|
+
if version == MQTTProtocolLevel.v5 {
|
|
230
|
+
data = try MQTT5Protocol.buildUnsubscribeV5(packetId: pid, topics: [topic])
|
|
231
|
+
} else {
|
|
232
|
+
data = try MQTTProtocol.buildUnsubscribe(packetId: pid, topics: [topic])
|
|
233
|
+
}
|
|
234
|
+
try await w.write(data)
|
|
235
|
+
try await w.drain()
|
|
236
|
+
|
|
237
|
+
let fixed = try await r.readexactly(2)
|
|
238
|
+
let (_, remLen, _) = try MQTTProtocol.parseFixedHeader(Data(fixed))
|
|
239
|
+
_ = try await r.readexactly(remLen)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public func disconnect() async throws {
|
|
243
|
+
let task = messageLoopTask
|
|
244
|
+
messageLoopTask = nil
|
|
245
|
+
task?.cancel()
|
|
246
|
+
_ = try? await task?.value
|
|
247
|
+
|
|
248
|
+
lock.lock()
|
|
249
|
+
let w = writer
|
|
250
|
+
let version = activeProtocolVersion
|
|
251
|
+
quicClient = nil
|
|
252
|
+
stream = nil
|
|
253
|
+
reader = nil
|
|
254
|
+
writer = nil
|
|
255
|
+
state = .disconnected
|
|
256
|
+
activeProtocolVersion = 0
|
|
257
|
+
lock.unlock()
|
|
258
|
+
|
|
259
|
+
if let w = w {
|
|
260
|
+
let data: Data
|
|
261
|
+
if version == MQTTProtocolLevel.v5 {
|
|
262
|
+
data = try MQTT5Protocol.buildDisconnectV5(reasonCode: .normalDisconnectionDisc)
|
|
263
|
+
} else {
|
|
264
|
+
data = MQTTProtocol.buildDisconnect()
|
|
265
|
+
}
|
|
266
|
+
try? await w.write(data)
|
|
267
|
+
try? await w.drain()
|
|
268
|
+
try? await w.close()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public func onMessage(_ topic: String, _ callback: @escaping (Data) -> Void) {
|
|
273
|
+
lock.lock()
|
|
274
|
+
subscribedTopics[topic] = callback
|
|
275
|
+
lock.unlock()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private func nextPacketIdUsed() -> UInt16 {
|
|
279
|
+
lock.lock()
|
|
280
|
+
defer { lock.unlock() }
|
|
281
|
+
let pid = nextPacketId
|
|
282
|
+
nextPacketId = nextPacketId &+ 1
|
|
283
|
+
if nextPacketId == 0 { nextPacketId = 1 }
|
|
284
|
+
return pid
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func startMessageLoop() {
|
|
288
|
+
messageLoopTask = Task { [weak self] in
|
|
289
|
+
guard let self = self else { return }
|
|
290
|
+
while !Task.isCancelled {
|
|
291
|
+
let r: MQTTStreamReaderProtocol?
|
|
292
|
+
self.lock.lock()
|
|
293
|
+
r = self.reader
|
|
294
|
+
self.lock.unlock()
|
|
295
|
+
guard let r = r else { break }
|
|
296
|
+
|
|
297
|
+
do {
|
|
298
|
+
let fixed = try await r.readexactly(2)
|
|
299
|
+
let (msgType, remLen, _) = try MQTTProtocol.parseFixedHeader(Data(fixed))
|
|
300
|
+
let rest = try await r.readexactly(remLen)
|
|
301
|
+
let type = msgType & 0xF0
|
|
302
|
+
|
|
303
|
+
self.lock.lock()
|
|
304
|
+
let version = self.activeProtocolVersion
|
|
305
|
+
let w = self.writer
|
|
306
|
+
self.lock.unlock()
|
|
307
|
+
|
|
308
|
+
switch type {
|
|
309
|
+
case MQTTMessageType.PINGREQ.rawValue:
|
|
310
|
+
if let w = w {
|
|
311
|
+
let pr = MQTTProtocol.buildPingresp()
|
|
312
|
+
try await w.write(Data(pr))
|
|
313
|
+
try await w.drain()
|
|
314
|
+
}
|
|
315
|
+
case MQTTMessageType.PUBLISH.rawValue:
|
|
316
|
+
let qos = (msgType >> 1) & 0x03
|
|
317
|
+
let (topic, packetId, payload, _) = try MQTTProtocol.parsePublish(Data(rest), offset: 0, qos: qos)
|
|
318
|
+
|
|
319
|
+
self.lock.lock()
|
|
320
|
+
let cb = self.subscribedTopics[topic]
|
|
321
|
+
self.lock.unlock()
|
|
322
|
+
cb?(payload)
|
|
323
|
+
|
|
324
|
+
if qos >= 1, let pid = packetId {
|
|
325
|
+
self.lock.lock()
|
|
326
|
+
let wPuback = self.writer
|
|
327
|
+
self.lock.unlock()
|
|
328
|
+
if let wPuback = wPuback {
|
|
329
|
+
let puback = MQTTProtocol.buildPuback(packetId: pid)
|
|
330
|
+
try await wPuback.write(Data(puback))
|
|
331
|
+
try await wPuback.drain()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
if !Task.isCancelled { break }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|