@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,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MQTT5ReasonCodes.swift
|
|
3
|
+
// MqttQuicPlugin
|
|
4
|
+
//
|
|
5
|
+
// MQTT 5.0 Reason Codes. Matches MQTTD mqttd/reason_codes.py.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public enum MQTT5ReasonCode: UInt8 {
|
|
11
|
+
// Success
|
|
12
|
+
case success = 0x00
|
|
13
|
+
case normalDisconnection = 0x00
|
|
14
|
+
case disconnectWithWillMessage = 0x04
|
|
15
|
+
|
|
16
|
+
// CONNACK Reason Codes
|
|
17
|
+
case unspecifiedError = 0x80
|
|
18
|
+
case malformedPacket = 0x81
|
|
19
|
+
case protocolError = 0x82
|
|
20
|
+
case implementationSpecificError = 0x83
|
|
21
|
+
case unsupportedProtocolVersion = 0x84
|
|
22
|
+
case clientIdentifierNotValid = 0x85
|
|
23
|
+
case badUserNameOrPassword = 0x86
|
|
24
|
+
case notAuthorized = 0x87
|
|
25
|
+
case serverUnavailable = 0x88
|
|
26
|
+
case serverBusy = 0x89
|
|
27
|
+
case banned = 0x8A
|
|
28
|
+
case badAuthenticationMethod = 0x8C
|
|
29
|
+
case topicNameInvalid = 0x90
|
|
30
|
+
case packetTooLarge = 0x95
|
|
31
|
+
case quotaExceeded = 0x97
|
|
32
|
+
case payloadFormatInvalid = 0x99
|
|
33
|
+
case retainNotSupported = 0x9A
|
|
34
|
+
case qosNotSupported = 0x9B
|
|
35
|
+
case useAnotherServer = 0x9C
|
|
36
|
+
case serverMoved = 0x9D
|
|
37
|
+
case connectionRateExceeded = 0x9F
|
|
38
|
+
|
|
39
|
+
// PUBACK, PUBREC, PUBREL, PUBCOMP Reason Codes
|
|
40
|
+
case noMatchingSubscribers = 0x10
|
|
41
|
+
case unspecifiedErrorPub = 0x80
|
|
42
|
+
case implementationSpecificErrorPub = 0x83
|
|
43
|
+
case notAuthorizedPub = 0x87
|
|
44
|
+
case topicNameInvalidPub = 0x90
|
|
45
|
+
case packetIdentifierInUse = 0x91
|
|
46
|
+
case quotaExceededPub = 0x97
|
|
47
|
+
case payloadFormatInvalidPub = 0x99
|
|
48
|
+
|
|
49
|
+
// SUBACK Reason Codes
|
|
50
|
+
case grantedQoS0 = 0x00
|
|
51
|
+
case grantedQoS1 = 0x01
|
|
52
|
+
case grantedQoS2 = 0x02
|
|
53
|
+
case unspecifiedErrorSub = 0x80
|
|
54
|
+
case implementationSpecificErrorSub = 0x83
|
|
55
|
+
case notAuthorizedSub = 0x87
|
|
56
|
+
case topicFilterInvalid = 0x8F
|
|
57
|
+
case packetIdentifierInUseSub = 0x91
|
|
58
|
+
case quotaExceededSub = 0x97
|
|
59
|
+
case sharedSubscriptionsNotSupported = 0x9E
|
|
60
|
+
case subscriptionIdentifiersNotSupported = 0xA1
|
|
61
|
+
case wildcardSubscriptionsNotSupported = 0xA2
|
|
62
|
+
|
|
63
|
+
// UNSUBACK Reason Codes
|
|
64
|
+
case successUnsub = 0x00
|
|
65
|
+
case noSubscriptionExisted = 0x11
|
|
66
|
+
case unspecifiedErrorUnsub = 0x80
|
|
67
|
+
case implementationSpecificErrorUnsub = 0x83
|
|
68
|
+
case notAuthorizedUnsub = 0x87
|
|
69
|
+
case topicFilterInvalidUnsub = 0x8F
|
|
70
|
+
case packetIdentifierInUseUnsub = 0x91
|
|
71
|
+
|
|
72
|
+
// DISCONNECT Reason Codes
|
|
73
|
+
case normalDisconnectionDisc = 0x00
|
|
74
|
+
case disconnectWithWillMessageDisc = 0x04
|
|
75
|
+
case unspecifiedErrorDisc = 0x80
|
|
76
|
+
case malformedPacketDisc = 0x81
|
|
77
|
+
case protocolErrorDisc = 0x82
|
|
78
|
+
case implementationSpecificErrorDisc = 0x83
|
|
79
|
+
case notAuthorizedDisc = 0x87
|
|
80
|
+
case serverBusyDisc = 0x89
|
|
81
|
+
case serverShuttingDown = 0x8B
|
|
82
|
+
case badAuthenticationMethodDisc = 0x8C
|
|
83
|
+
case keepAliveTimeout = 0x8D
|
|
84
|
+
case sessionTakenOver = 0x8E
|
|
85
|
+
case topicFilterInvalidDisc = 0x8F
|
|
86
|
+
case topicNameInvalidDisc = 0x90
|
|
87
|
+
case receiveMaximumExceeded = 0x93
|
|
88
|
+
case topicAliasInvalid = 0x94
|
|
89
|
+
case packetTooLargeDisc = 0x95
|
|
90
|
+
case messageRateTooHigh = 0x96
|
|
91
|
+
case quotaExceededDisc = 0x97
|
|
92
|
+
case administrativeAction = 0x98
|
|
93
|
+
case payloadFormatInvalidDisc = 0x99
|
|
94
|
+
case retainNotSupportedDisc = 0x9A
|
|
95
|
+
case qosNotSupportedDisc = 0x9B
|
|
96
|
+
case useAnotherServerDisc = 0x9C
|
|
97
|
+
case serverMovedDisc = 0x9D
|
|
98
|
+
case sharedSubscriptionsNotSupportedDisc = 0x9E
|
|
99
|
+
case connectionRateExceededDisc = 0x9F
|
|
100
|
+
case maximumConnectTime = 0xA0
|
|
101
|
+
case subscriptionIdentifiersNotSupportedDisc = 0xA1
|
|
102
|
+
case wildcardSubscriptionsNotSupportedDisc = 0xA2
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Compatibility mapping for MQTT 3.1.1 return codes
|
|
106
|
+
public let MQTT3_TO_MQTT5_REASON_CODE: [UInt8: MQTT5ReasonCode] = [
|
|
107
|
+
0x00: .success,
|
|
108
|
+
0x01: .unsupportedProtocolVersion,
|
|
109
|
+
0x02: .clientIdentifierNotValid,
|
|
110
|
+
0x03: .serverUnavailable,
|
|
111
|
+
0x04: .badUserNameOrPassword,
|
|
112
|
+
0x05: .notAuthorized,
|
|
113
|
+
]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MQTTProtocol.swift
|
|
3
|
+
// MqttQuicPlugin
|
|
4
|
+
//
|
|
5
|
+
// MQTT 3.1.1 encode/decode. Matches MQTTD mqttd/protocol.py.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public enum MQTTProtocolError: Error {
|
|
11
|
+
case insufficientData(String)
|
|
12
|
+
case invalidRemainingLength(Int)
|
|
13
|
+
case invalidUTF8
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public final class MQTTProtocol {
|
|
17
|
+
|
|
18
|
+
public static let protocolName = "MQTT"
|
|
19
|
+
|
|
20
|
+
// MARK: - Remaining length
|
|
21
|
+
|
|
22
|
+
/// Encode remaining length (variable-length, 1–4 bytes). Max 268_435_455.
|
|
23
|
+
public static func encodeRemainingLength(_ length: Int) throws -> [UInt8] {
|
|
24
|
+
if length < 0 || length > 268_435_455 {
|
|
25
|
+
throw MQTTProtocolError.invalidRemainingLength(length)
|
|
26
|
+
}
|
|
27
|
+
var enc: [UInt8] = []
|
|
28
|
+
var n = length
|
|
29
|
+
repeat {
|
|
30
|
+
var b = UInt8(n % 128)
|
|
31
|
+
n /= 128
|
|
32
|
+
if n > 0 { b |= 0x80 }
|
|
33
|
+
enc.append(b)
|
|
34
|
+
} while n > 0
|
|
35
|
+
return enc
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Decode remaining length. Returns (length, bytesConsumed).
|
|
39
|
+
public static func decodeRemainingLength(_ data: Data, offset: Int = 0) throws -> (Int, Int) {
|
|
40
|
+
var mul: Int = 1
|
|
41
|
+
var len: Int = 0
|
|
42
|
+
var i = offset
|
|
43
|
+
for _ in 0..<4 {
|
|
44
|
+
if i >= data.count { throw MQTTProtocolError.insufficientData("remaining length") }
|
|
45
|
+
let b = data[i]
|
|
46
|
+
len += Int(b & 0x7F) * mul
|
|
47
|
+
i += 1
|
|
48
|
+
if (b & 0x80) == 0 { break }
|
|
49
|
+
mul *= 128
|
|
50
|
+
}
|
|
51
|
+
return (len, i - offset)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - String (2-byte length + UTF-8)
|
|
55
|
+
|
|
56
|
+
public static func encodeString(_ s: String) throws -> Data {
|
|
57
|
+
guard let utf8 = s.data(using: .utf8) else { throw MQTTProtocolError.invalidUTF8 }
|
|
58
|
+
let count = utf8.count
|
|
59
|
+
if count > 0xFFFF { throw MQTTProtocolError.insufficientData("string too long") }
|
|
60
|
+
var out = Data(capacity: 2 + count)
|
|
61
|
+
out.append(UInt8((count >> 8) & 0xFF))
|
|
62
|
+
out.append(UInt8(count & 0xFF))
|
|
63
|
+
out.append(utf8)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Returns (string, newOffset).
|
|
68
|
+
public static func decodeString(_ data: Data, offset: Int) throws -> (String, Int) {
|
|
69
|
+
if offset + 2 > data.count { throw MQTTProtocolError.insufficientData("string length") }
|
|
70
|
+
let hi = Int(data[offset] & 0xFF)
|
|
71
|
+
let lo = Int(data[offset + 1] & 0xFF)
|
|
72
|
+
let strLen = (hi << 8) | lo
|
|
73
|
+
let start = offset + 2
|
|
74
|
+
if start + strLen > data.count { throw MQTTProtocolError.insufficientData("string content") }
|
|
75
|
+
let sub = data.subdata(in: start..<(start + strLen))
|
|
76
|
+
guard let s = String(data: sub, encoding: .utf8) else { throw MQTTProtocolError.invalidUTF8 }
|
|
77
|
+
return (s, start + strLen)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// MARK: - Fixed header
|
|
81
|
+
|
|
82
|
+
/// (messageType, remainingLength, bytesConsumed)
|
|
83
|
+
public static func parseFixedHeader(_ data: Data) throws -> (UInt8, Int, Int) {
|
|
84
|
+
if data.count < 2 { throw MQTTProtocolError.insufficientData("fixed header") }
|
|
85
|
+
let msgType = data[0]
|
|
86
|
+
let (rem, consumed) = try decodeRemainingLength(data, offset: 1)
|
|
87
|
+
return (msgType, rem, 1 + consumed)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - CONNECT (client → server)
|
|
91
|
+
|
|
92
|
+
public static func buildConnect(
|
|
93
|
+
clientId: String,
|
|
94
|
+
username: String? = nil,
|
|
95
|
+
password: String? = nil,
|
|
96
|
+
keepalive: UInt16 = 60,
|
|
97
|
+
cleanSession: Bool = true
|
|
98
|
+
) throws -> Data {
|
|
99
|
+
var variableHeader = Data()
|
|
100
|
+
variableHeader.append(try encodeString(Self.protocolName))
|
|
101
|
+
variableHeader.append(MQTTProtocolLevel.v311)
|
|
102
|
+
var flags: UInt8 = 0
|
|
103
|
+
if cleanSession { flags |= MQTTConnectFlags.cleanSession }
|
|
104
|
+
if username != nil { flags |= MQTTConnectFlags.username }
|
|
105
|
+
if password != nil { flags |= MQTTConnectFlags.password }
|
|
106
|
+
variableHeader.append(flags)
|
|
107
|
+
variableHeader.append(UInt8((keepalive >> 8) & 0xFF))
|
|
108
|
+
variableHeader.append(UInt8(keepalive & 0xFF))
|
|
109
|
+
|
|
110
|
+
var payload = Data()
|
|
111
|
+
payload.append(try encodeString(clientId))
|
|
112
|
+
if let u = username { payload.append(try encodeString(u)) }
|
|
113
|
+
if let p = password { payload.append(try encodeString(p)) }
|
|
114
|
+
|
|
115
|
+
let remLen = variableHeader.count + payload.count
|
|
116
|
+
var fixed = Data()
|
|
117
|
+
fixed.append(MQTTMessageType.CONNECT.rawValue)
|
|
118
|
+
fixed.append(contentsOf: try encodeRemainingLength(remLen))
|
|
119
|
+
|
|
120
|
+
var out = Data()
|
|
121
|
+
out.append(fixed)
|
|
122
|
+
out.append(variableHeader)
|
|
123
|
+
out.append(payload)
|
|
124
|
+
return out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - CONNACK (server → client)
|
|
128
|
+
|
|
129
|
+
public static func buildConnack(returnCode: UInt8 = MQTTConnAckCode.accepted.rawValue) -> Data {
|
|
130
|
+
var out = Data()
|
|
131
|
+
out.append(MQTTMessageType.CONNACK.rawValue)
|
|
132
|
+
out.append(contentsOf: try! encodeRemainingLength(2))
|
|
133
|
+
out.append(0x00) // flags
|
|
134
|
+
out.append(returnCode)
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Parse CONNACK variable header. Assumes fixed header already consumed.
|
|
139
|
+
/// Returns (sessionPresent, returnCode).
|
|
140
|
+
public static func parseConnack(_ data: Data, offset: Int = 0) throws -> (Bool, UInt8) {
|
|
141
|
+
if offset + 2 > data.count { throw MQTTProtocolError.insufficientData("CONNACK") }
|
|
142
|
+
let flags = data[offset]
|
|
143
|
+
let rc = data[offset + 1]
|
|
144
|
+
return ((flags & 0x01) != 0, rc)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - PUBLISH
|
|
148
|
+
|
|
149
|
+
public static func buildPublish(
|
|
150
|
+
topic: String,
|
|
151
|
+
payload: Data,
|
|
152
|
+
packetId: UInt16? = nil,
|
|
153
|
+
qos: UInt8 = 0,
|
|
154
|
+
retain: Bool = false
|
|
155
|
+
) throws -> Data {
|
|
156
|
+
var msgType = MQTTMessageType.PUBLISH.rawValue
|
|
157
|
+
if qos > 0 { msgType |= (qos << 1) }
|
|
158
|
+
if retain { msgType |= 0x01 }
|
|
159
|
+
|
|
160
|
+
var vh = Data()
|
|
161
|
+
vh.append(try encodeString(topic))
|
|
162
|
+
if qos > 0, let pid = packetId {
|
|
163
|
+
vh.append(UInt8((pid >> 8) & 0xFF))
|
|
164
|
+
vh.append(UInt8(pid & 0xFF))
|
|
165
|
+
}
|
|
166
|
+
var pl = vh
|
|
167
|
+
pl.append(payload)
|
|
168
|
+
let remLen = pl.count
|
|
169
|
+
var out = Data()
|
|
170
|
+
out.append(msgType)
|
|
171
|
+
out.append(contentsOf: try encodeRemainingLength(remLen))
|
|
172
|
+
out.append(pl)
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Parse PUBLISH payload (after fixed header). Assumes fixed header already parsed for QoS.
|
|
177
|
+
/// Returns (topic, packetId?, payload, newOffset). packetId only for QoS > 0.
|
|
178
|
+
public static func parsePublish(_ data: Data, offset: Int, qos: UInt8) throws -> (String, UInt16?, Data, Int) {
|
|
179
|
+
var off = offset
|
|
180
|
+
let (topic, next) = try decodeString(data, offset: off)
|
|
181
|
+
off = next
|
|
182
|
+
var pid: UInt16? = nil
|
|
183
|
+
if qos > 0 {
|
|
184
|
+
if off + 2 > data.count { throw MQTTProtocolError.insufficientData("PUBLISH packet ID") }
|
|
185
|
+
pid = (UInt16(data[off]) << 8) | UInt16(data[off + 1])
|
|
186
|
+
off += 2
|
|
187
|
+
}
|
|
188
|
+
let payload = data.subdata(in: off..<data.count)
|
|
189
|
+
return (topic, pid, payload, data.count)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// MARK: - PUBACK
|
|
193
|
+
|
|
194
|
+
public static func buildPuback(packetId: UInt16) -> Data {
|
|
195
|
+
var out = Data()
|
|
196
|
+
out.append(MQTTMessageType.PUBACK.rawValue)
|
|
197
|
+
out.append(contentsOf: try! encodeRemainingLength(2))
|
|
198
|
+
out.append(UInt8((packetId >> 8) & 0xFF))
|
|
199
|
+
out.append(UInt8(packetId & 0xFF))
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public static func parsePuback(_ data: Data, offset: Int = 0) throws -> UInt16 {
|
|
204
|
+
if offset + 2 > data.count { throw MQTTProtocolError.insufficientData("PUBACK") }
|
|
205
|
+
return (UInt16(data[offset]) << 8) | UInt16(data[offset + 1])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// MARK: - SUBSCRIBE
|
|
209
|
+
|
|
210
|
+
public static func buildSubscribe(packetId: UInt16, topic: String, qos: UInt8 = 0) throws -> Data {
|
|
211
|
+
var out = Data()
|
|
212
|
+
out.append(MQTTMessageType.SUBSCRIBE.rawValue | 0x02) // QoS 1 required
|
|
213
|
+
var vh = Data()
|
|
214
|
+
vh.append(UInt8((packetId >> 8) & 0xFF))
|
|
215
|
+
vh.append(UInt8(packetId & 0xFF))
|
|
216
|
+
var pl = Data()
|
|
217
|
+
pl.append(try encodeString(topic))
|
|
218
|
+
pl.append(qos & 0x03)
|
|
219
|
+
let rem = vh.count + pl.count
|
|
220
|
+
out.append(contentsOf: try encodeRemainingLength(rem))
|
|
221
|
+
out.append(vh)
|
|
222
|
+
out.append(pl)
|
|
223
|
+
return out
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Parse SUBSCRIBE. Returns (packetId, topic, qos, newOffset).
|
|
227
|
+
public static func parseSubscribe(_ data: Data, offset: Int = 0) throws -> (UInt16, String, UInt8, Int) {
|
|
228
|
+
var off = offset
|
|
229
|
+
if off + 2 > data.count { throw MQTTProtocolError.insufficientData("SUBSCRIBE packet ID") }
|
|
230
|
+
let pid = (UInt16(data[off]) << 8) | UInt16(data[off + 1])
|
|
231
|
+
off += 2
|
|
232
|
+
let (topic, next) = try decodeString(data, offset: off)
|
|
233
|
+
off = next
|
|
234
|
+
if off >= data.count { throw MQTTProtocolError.insufficientData("SUBSCRIBE QoS") }
|
|
235
|
+
let qos = data[off] & 0x03
|
|
236
|
+
off += 1
|
|
237
|
+
return (pid, topic, qos, off)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// MARK: - SUBACK
|
|
241
|
+
|
|
242
|
+
public static func buildSuback(packetId: UInt16, returnCode: UInt8 = 0) -> Data {
|
|
243
|
+
var out = Data()
|
|
244
|
+
out.append(MQTTMessageType.SUBACK.rawValue)
|
|
245
|
+
out.append(contentsOf: try! encodeRemainingLength(3))
|
|
246
|
+
out.append(UInt8((packetId >> 8) & 0xFF))
|
|
247
|
+
out.append(UInt8(packetId & 0xFF))
|
|
248
|
+
out.append(returnCode)
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Parse SUBACK. Returns (packetId, returnCode, newOffset).
|
|
253
|
+
public static func parseSuback(_ data: Data, offset: Int = 0) throws -> (UInt16, UInt8, Int) {
|
|
254
|
+
if offset + 3 > data.count { throw MQTTProtocolError.insufficientData("SUBACK") }
|
|
255
|
+
let pid = (UInt16(data[offset]) << 8) | UInt16(data[offset + 1])
|
|
256
|
+
let rc = data[offset + 2]
|
|
257
|
+
return (pid, rc, offset + 3)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// MARK: - UNSUBSCRIBE / UNSUBACK
|
|
261
|
+
|
|
262
|
+
public static func buildUnsubscribe(packetId: UInt16, topics: [String]) throws -> Data {
|
|
263
|
+
var vh = Data()
|
|
264
|
+
vh.append(UInt8((packetId >> 8) & 0xFF))
|
|
265
|
+
vh.append(UInt8(packetId & 0xFF))
|
|
266
|
+
var pl = Data()
|
|
267
|
+
for t in topics { pl.append(try encodeString(t)) }
|
|
268
|
+
let rem = vh.count + pl.count
|
|
269
|
+
var out = Data()
|
|
270
|
+
out.append(MQTTMessageType.UNSUBSCRIBE.rawValue | 0x02)
|
|
271
|
+
out.append(contentsOf: try encodeRemainingLength(rem))
|
|
272
|
+
out.append(vh)
|
|
273
|
+
out.append(pl)
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public static func buildUnsuback(packetId: UInt16) -> Data {
|
|
278
|
+
var out = Data()
|
|
279
|
+
out.append(MQTTMessageType.UNSUBACK.rawValue)
|
|
280
|
+
out.append(contentsOf: try! encodeRemainingLength(2))
|
|
281
|
+
out.append(UInt8((packetId >> 8) & 0xFF))
|
|
282
|
+
out.append(UInt8(packetId & 0xFF))
|
|
283
|
+
return out
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public static func parseUnsubscribe(_ data: Data, offset: Int = 0) throws -> (UInt16, [String], Int) {
|
|
287
|
+
var off = offset
|
|
288
|
+
if off + 2 > data.count { throw MQTTProtocolError.insufficientData("UNSUBSCRIBE packet ID") }
|
|
289
|
+
let pid = (UInt16(data[off]) << 8) | UInt16(data[off + 1])
|
|
290
|
+
off += 2
|
|
291
|
+
var topics: [String] = []
|
|
292
|
+
while off < data.count {
|
|
293
|
+
let (t, next) = try decodeString(data, offset: off)
|
|
294
|
+
topics.append(t)
|
|
295
|
+
off = next
|
|
296
|
+
}
|
|
297
|
+
return (pid, topics, off)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// MARK: - PINGREQ / PINGRESP / DISCONNECT
|
|
301
|
+
|
|
302
|
+
public static func buildPingreq() -> Data {
|
|
303
|
+
var out = Data()
|
|
304
|
+
out.append(MQTTMessageType.PINGREQ.rawValue)
|
|
305
|
+
out.append(contentsOf: try! encodeRemainingLength(0))
|
|
306
|
+
return out
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
public static func buildPingresp() -> Data {
|
|
310
|
+
var out = Data()
|
|
311
|
+
out.append(MQTTMessageType.PINGRESP.rawValue)
|
|
312
|
+
out.append(contentsOf: try! encodeRemainingLength(0))
|
|
313
|
+
return out
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public static func buildDisconnect() -> Data {
|
|
317
|
+
var out = Data()
|
|
318
|
+
out.append(MQTTMessageType.DISCONNECT.rawValue)
|
|
319
|
+
out.append(contentsOf: try! encodeRemainingLength(0))
|
|
320
|
+
return out
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MQTTTypes.swift
|
|
3
|
+
// MqttQuicPlugin
|
|
4
|
+
//
|
|
5
|
+
// MQTT packet types and constants. Matches MQTTD protocol.py / protocol_v5.py.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
/// MQTT 3.1.1 / 5.0 message types (fixed header first nibble)
|
|
11
|
+
public enum MQTTMessageType: UInt8 {
|
|
12
|
+
case CONNECT = 0x10
|
|
13
|
+
case CONNACK = 0x20
|
|
14
|
+
case PUBLISH = 0x30
|
|
15
|
+
case PUBACK = 0x40
|
|
16
|
+
case PUBREC = 0x50
|
|
17
|
+
case PUBREL = 0x62
|
|
18
|
+
case PUBCOMP = 0x70
|
|
19
|
+
case SUBSCRIBE = 0x82
|
|
20
|
+
case SUBACK = 0x90
|
|
21
|
+
case UNSUBSCRIBE = 0xA2
|
|
22
|
+
case UNSUBACK = 0xB0
|
|
23
|
+
case PINGREQ = 0xC0
|
|
24
|
+
case PINGRESP = 0xD0
|
|
25
|
+
case DISCONNECT = 0xE0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// CONNECT flags
|
|
29
|
+
public struct MQTTConnectFlags {
|
|
30
|
+
public static let username: UInt8 = 0x80
|
|
31
|
+
public static let password: UInt8 = 0x40
|
|
32
|
+
public static let willRetain: UInt8 = 0x20
|
|
33
|
+
public static let willQoS1: UInt8 = 0x08
|
|
34
|
+
public static let willQoS2: UInt8 = 0x18
|
|
35
|
+
public static let willFlag: UInt8 = 0x04
|
|
36
|
+
public static let cleanSession: UInt8 = 0x02
|
|
37
|
+
public static let reserved: UInt8 = 0x01
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// CONNACK return codes (MQTT 3.1.1)
|
|
41
|
+
public enum MQTTConnAckCode: UInt8 {
|
|
42
|
+
case accepted = 0x00
|
|
43
|
+
case unacceptableProtocol = 0x01
|
|
44
|
+
case identifierRejected = 0x02
|
|
45
|
+
case serverUnavailable = 0x03
|
|
46
|
+
case badUsernamePassword = 0x04
|
|
47
|
+
case notAuthorized = 0x05
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Protocol levels
|
|
51
|
+
public struct MQTTProtocolLevel {
|
|
52
|
+
public static let v311: UInt8 = 0x04
|
|
53
|
+
public static let v5: UInt8 = 0x05
|
|
54
|
+
}
|