homebridge-roborock-vacuum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/config.schema.json +31 -0
  5. package/dist/index.js +10 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/logger.js +39 -0
  8. package/dist/logger.js.map +1 -0
  9. package/dist/platform.js +167 -0
  10. package/dist/platform.js.map +1 -0
  11. package/dist/settings.js +8 -0
  12. package/dist/settings.js.map +1 -0
  13. package/dist/types.js +3 -0
  14. package/dist/types.js.map +1 -0
  15. package/dist/vacuum_accessory.js +152 -0
  16. package/dist/vacuum_accessory.js.map +1 -0
  17. package/package.json +66 -0
  18. package/roborockLib/data/UserData +4 -0
  19. package/roborockLib/data/clientID +4 -0
  20. package/roborockLib/i18n/de/translations.json +188 -0
  21. package/roborockLib/i18n/en/translations.json +208 -0
  22. package/roborockLib/i18n/es/translations.json +188 -0
  23. package/roborockLib/i18n/fr/translations.json +188 -0
  24. package/roborockLib/i18n/it/translations.json +188 -0
  25. package/roborockLib/i18n/nl/translations.json +188 -0
  26. package/roborockLib/i18n/pl/translations.json +188 -0
  27. package/roborockLib/i18n/pt/translations.json +188 -0
  28. package/roborockLib/i18n/ru/translations.json +188 -0
  29. package/roborockLib/i18n/uk/translations.json +188 -0
  30. package/roborockLib/i18n/zh-cn/translations.json +188 -0
  31. package/roborockLib/lib/RRMapParser.js +447 -0
  32. package/roborockLib/lib/deviceFeatures.js +995 -0
  33. package/roborockLib/lib/localConnector.js +249 -0
  34. package/roborockLib/lib/map/map.html +110 -0
  35. package/roborockLib/lib/map/zones.js +713 -0
  36. package/roborockLib/lib/mapCreator.js +692 -0
  37. package/roborockLib/lib/message.js +223 -0
  38. package/roborockLib/lib/messageQueueHandler.js +87 -0
  39. package/roborockLib/lib/roborockPackageHelper.js +116 -0
  40. package/roborockLib/lib/roborock_mqtt_connector.js +349 -0
  41. package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
  42. package/roborockLib/lib/vacuum.js +636 -0
  43. package/roborockLib/roborockAPI.js +1365 -0
  44. package/roborockLib/test.js +31 -0
  45. package/roborockLib/userdata.json +24 -0
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+
3
+ const mqtt = require("mqtt");
4
+ const crypto = require("crypto");
5
+ const Parser = require("binary-parser").Parser;
6
+ const zlib = require("zlib");
7
+ const forge = require("node-forge");
8
+
9
+ const protocol301Parser = new Parser()
10
+ .endianess("little")
11
+ .string("endpoint", {
12
+ length: 15,
13
+ stripNull: true,
14
+ })
15
+ .uint8("unknown1")
16
+ .uint16("id")
17
+ .buffer("unknown2", {
18
+ length: 6,
19
+ });
20
+
21
+ const photoParser = new Parser()
22
+ .endianess("little")
23
+ .string("roborock", {
24
+ length: 8,
25
+ stripNull: true,
26
+ })
27
+ .uint8("id");
28
+
29
+ let mqttUser;
30
+ let mqttPassword;
31
+ let client;
32
+ let endpoint;
33
+ let rriot;
34
+
35
+ let photoGzipChunks = [];
36
+ let photoChunkID = 0;
37
+
38
+ class roborock_mqtt_connector {
39
+ constructor(adapter) {
40
+ this.adapter = adapter;
41
+
42
+ this.connected = false;
43
+
44
+ const keypair = forge.pki.rsa.generateKeyPair(2048);
45
+ this.keys = {
46
+ public: { n: null, e: null },
47
+ private: {
48
+ n: null,
49
+ e: null,
50
+ d: null,
51
+ p: null,
52
+ q: null,
53
+ dmp1: null,
54
+ dmq1: null,
55
+ coeff: null,
56
+ },
57
+ };
58
+
59
+ // Convert the keys to the desired format
60
+ this.keys.public.n = keypair.publicKey.n.toString(16);
61
+ this.keys.public.e = keypair.publicKey.e.toString(16);
62
+ this.keys.private.n = keypair.privateKey.n.toString(16);
63
+ this.keys.private.e = keypair.privateKey.e.toString(16);
64
+ this.keys.private.d = keypair.privateKey.d.toString(16);
65
+ this.keys.private.p = keypair.privateKey.p.toString(16);
66
+ this.keys.private.q = keypair.privateKey.q.toString(16);
67
+ this.keys.private.dmp1 = keypair.privateKey.dP.toString(16);
68
+ this.keys.private.dmq1 = keypair.privateKey.dQ.toString(16);
69
+ this.keys.private.coeff = keypair.privateKey.qInv.toString(16);
70
+ }
71
+
72
+ async initUser(userdata) {
73
+ rriot = userdata.rriot;
74
+
75
+ endpoint = this.md5bin(rriot.k).subarray(8, 14).toString("base64"); // Could be a random but rather static string. The app generates it on first run.
76
+ mqttUser = this.md5hex(rriot.u + ":" + rriot.k).substring(2, 10);
77
+ mqttPassword = this.md5hex(rriot.s + ":" + rriot.k).substring(16);
78
+ client = mqtt.connect(rriot.r.m, {
79
+ clientId: mqttUser,
80
+ username: mqttUser,
81
+ password: mqttPassword,
82
+ keepalive: 30,
83
+ });
84
+ }
85
+
86
+ async initMQTT_Subscribe() {
87
+ const timeout = setTimeout(async () => {
88
+ this.adapter.restart();
89
+ }, 30000);
90
+
91
+ await client.on("connect", (result) => {
92
+ if (typeof result != "undefined") {
93
+ client.subscribe(`rr/m/o/${rriot.u}/${mqttUser}/#`, (err, granted) => {
94
+ if (err) {
95
+ this.adapter.catchError(`Failed to subscribe to Roborock MQTT Server! Error: ${err}, granted: ${JSON.stringify(granted)}`, `client.on("connect")`);
96
+ }
97
+ });
98
+ clearTimeout(timeout);
99
+
100
+ this.connected = true;
101
+ }
102
+ this.adapter.log.debug(`MQTT connection connected ${JSON.stringify(result)}.`);
103
+ });
104
+
105
+ await client.on("error", (result) => {
106
+ this.adapter.catchError(`MQTT connection error: ${result}`, `client.on("error")`);
107
+
108
+ this.connected = false;
109
+ });
110
+
111
+ await client.on("close", () => {
112
+ this.adapter.log.info(`MQTT connection close.`);
113
+
114
+ this.connected = false;
115
+ });
116
+
117
+ await client.on("reconnect", (error) => {
118
+ if (error) {
119
+ this.adapter.catchError(`Failed to reconnect to MQTT server.`, `mqtt client reconnect`);
120
+ } else {
121
+ client.subscribe(`rr/m/o/${rriot.u}/${mqttUser}/#`, (err, granted) => {
122
+ if (err) {
123
+ this.adapter.catchError(`Failed to subscribe to Roborock MQTT Server! Error: ${err}, granted: ${JSON.stringify(granted)}`, `client.on("reconnect")`);
124
+ }
125
+ });
126
+ clearTimeout(timeout);
127
+ }
128
+ this.adapter.log.info(`MQTT connection reconnect.`);
129
+ });
130
+
131
+ await client.on("offline", (result) => {
132
+ this.adapter.catchError(`MQTT connection offline: ${result}`, `client.on("offline")`);
133
+
134
+ this.connected = false;
135
+ });
136
+ }
137
+
138
+ async isArray(what) {
139
+ return Object.prototype.toString.call(what) === "[object Array]";
140
+ }
141
+
142
+ async initMQTT_Message() {
143
+ this.adapter.log.info(`MQTT initialized`);
144
+
145
+ client.on("message", (topic, message) => {
146
+ try {
147
+ const duid = topic.split("/").slice(-1)[0];
148
+ const data = this.adapter.message._decodeMsg(message, duid);
149
+ // this.adapter.log.debug(`MESSAGE RECEIVED for duid ${duid} with key: ${this.adapter.localKeys.get(duid)} data: ${JSON.stringify(data)} raw: ${JSON.stringify(mqttMessageParser.parse(message))} message: ${message}`);
150
+ // this.adapter.log.debug(`MESSAGE RECEIVED for duid ${duid} with key: ${this.adapter.localKeys.get(duid)} data: ${JSON.stringify(data.toString("hex"))} message: ${message}`);
151
+ // this.adapter.log.debug(`MESSAGE RECEIVED for duid ${duid} with key: ${this.adapter.localKeys.get(duid)} data: ${JSON.stringify(data)}`);
152
+
153
+ // this.adapter.log.debug("Protocol: " + data.protocol);
154
+ if (data.protocol == 102) {
155
+ // sometimes JSON.parse(data.payload).dps["102"] is not a JSON. Check for this!
156
+ let dps;
157
+ if (typeof JSON.parse(data.payload).dps["102"] != "undefined") {
158
+ dps = JSON.parse(JSON.parse(data.payload).dps["102"]);
159
+ } else {
160
+ dps = JSON.parse(data.payload).dps;
161
+ }
162
+
163
+ if(dps.id !== undefined){
164
+ this.adapter.log.debug(`Cloud message with protocol 102 and id ${dps.id} received. Result: ${JSON.stringify(dps.result)}`);
165
+ this.adapter.setStateAsync("CloudMessage", dps.result);
166
+ }
167
+ else{
168
+ this.adapter.log.debug(`Cloud message with protocol 102 received. Result: ${data.payload}`);
169
+
170
+ if(this.adapter.deviceNotify !== undefined){
171
+ this.adapter.deviceNotify("CloudMessage", JSON.parse(data.payload));
172
+ }
173
+
174
+ }
175
+
176
+
177
+ // special check for secure request like get_map_v1 etc. Don't process if result is OK. Instead wait for the actual response for protocol 301
178
+ if (dps.result != "ok") {
179
+ if (this.adapter.pendingRequests.has(dps.id)) {
180
+ const { resolve, timeout } = this.adapter.pendingRequests.get(dps.id);
181
+ this.adapter.clearTimeout(timeout);
182
+ this.adapter.pendingRequests.delete(dps.id);
183
+ resolve(dps.result);
184
+ }
185
+ }
186
+ // protocol 300 seems to be for get_photo 0 only. get_photo 0 is for large images. 1 is for small images.
187
+ } else if (data.protocol == 300) {
188
+ if (data.payload.subarray(0, 8) == "ROBOROCK") {
189
+ const photoData = photoParser.parse(data.payload);
190
+
191
+ if (this.adapter.pendingRequests.has(photoData.id)) {
192
+ this.adapter.log.debug(`First photo gzip chunk detected!`);
193
+
194
+ photoGzipChunks.push(data.payload.slice(56));
195
+ photoChunkID = photoData.id;
196
+ }
197
+ }
198
+ } else if (data.protocol == 301) {
199
+ const data2 = protocol301Parser.parse(data.payload.subarray(0, 24));
200
+
201
+ if (data.seq == 2 && photoGzipChunks != [] && photoChunkID != 0) {
202
+ this.adapter.log.debug(`Second photo gzip chunk detected!`);
203
+ photoGzipChunks.push(data.payload);
204
+
205
+ if (this.adapter.pendingRequests.has(photoChunkID)) {
206
+ const { resolve, timeout } = this.adapter.pendingRequests.get(photoChunkID);
207
+ this.adapter.clearTimeout(timeout);
208
+ this.adapter.pendingRequests.delete(photoChunkID);
209
+
210
+ const finalPhotoGzip = Buffer.concat(photoGzipChunks);
211
+
212
+ photoGzipChunks = [];
213
+ photoChunkID = 0;
214
+
215
+ resolve(finalPhotoGzip);
216
+ }
217
+ } else {
218
+ if (data.payload.subarray(0, 8) == "ROBOROCK") {
219
+ const photoData = photoParser.parse(data.payload);
220
+ this.adapter.log.debug(`Cloud message with protocol 301 and photo id ${photoData.id} received.`);
221
+
222
+ if (this.adapter.pendingRequests.has(photoData.id)) {
223
+ const { resolve, timeout } = this.adapter.pendingRequests.get(photoData.id);
224
+ this.adapter.clearTimeout(timeout);
225
+ this.adapter.pendingRequests.delete(photoData.id);
226
+ this.adapter.log.debug(`Cloud message with protocol 301 and photo id ${photoData.id} received.`);
227
+ resolve(data.payload.slice(56));
228
+ }
229
+ } else if (endpoint.startsWith(data2.endpoint)) {
230
+ const iv = Buffer.alloc(16, 0);
231
+ const decipher = crypto.createDecipheriv("aes-128-cbc", this.adapter.nonce, iv);
232
+ let decrypted = Buffer.concat([decipher.update(data.payload.subarray(24)), decipher.final()]);
233
+ decrypted = zlib.gunzipSync(decrypted);
234
+ // this.adapter.log.debug("raw 301: " + decrypted);
235
+
236
+ if (this.adapter.pendingRequests.has(data2.id)) {
237
+ const { resolve, timeout } = this.adapter.pendingRequests.get(data2.id);
238
+ this.adapter.clearTimeout(timeout);
239
+ this.adapter.pendingRequests.delete(data2.id);
240
+ // this.adapter.log.debug("protocol 301 OK check: " + JSON.stringify(decrypted));
241
+ this.adapter.log.debug(`Cloud message with protocol 301 and id ${data2.id} received.`);
242
+ resolve(decrypted);
243
+ }
244
+ }
245
+ }
246
+ } else if (data.protocol == 500) { // 500 is for general information
247
+ const dataString = data.payload.toString("utf8");
248
+ let parsedData;
249
+
250
+ try {
251
+ parsedData = JSON.parse(dataString);
252
+ } catch (error) {
253
+ // If parsing fails, the data might be corrupted or in an unexpected format
254
+ this.adapter.log.warn(`Unable to parse message for ${duid}. Error: ${error.message}. Data: ${dataString}`);
255
+ return;
256
+ }
257
+
258
+ // Check if the device is online
259
+ if (parsedData.online == false) {
260
+ this.adapter.log.info(`Couldn't process message. The device ${duid} is offline.`);
261
+ } else if (parsedData.online == true) {
262
+ // this.adapter.log.info(`Device ${duid} is online.`);
263
+ } else if (
264
+ // Check for firmware update information
265
+ parsedData.mqttOtaData
266
+ ) {
267
+ const otaStatus = parsedData.mqttOtaData.mqttOtaStatus?.status;
268
+ const otaProgress = parsedData.mqttOtaData.mqttOtaProgress?.progress;
269
+
270
+ if (otaStatus) {
271
+ this.adapter.log.info(`Device ${duid} firmware update status: ${otaStatus}`);
272
+ }
273
+
274
+ if (otaProgress !== undefined) {
275
+ this.adapter.log.info(`Device ${duid} firmware update progress: ${otaProgress}%`);
276
+ }
277
+ } else {
278
+ // Received an unrecognized message
279
+ this.adapter.log.warn(`Received an unrecognized message for ${duid}. Data: ${dataString}`);
280
+ }
281
+ }
282
+ else {
283
+ this.adapter.log.debug(`Received message with unknown protocol ${data.protocol} data: ${JSON.stringify(data)}.`);
284
+ }
285
+ } catch (error) {
286
+ this.adapter.log.error(`client.on message: ${error.stack} with topic ${topic} and message ${message.toString("hex")}`);
287
+ }
288
+ });
289
+ }
290
+
291
+ _encodeTimestamp(timestamp) {
292
+ const hex = timestamp.toString(16).padStart(8, "0").split("");
293
+ return [5, 6, 3, 7, 1, 2, 0, 4].map((idx) => hex[idx]).join("");
294
+ }
295
+
296
+ getEndpoint() {
297
+ return endpoint;
298
+ }
299
+
300
+ sendMessage(duid, roborockMessage) {
301
+ client.publish(`rr/m/i/${rriot.u}/${mqttUser}/${duid}`, roborockMessage, { qos: 1 });
302
+ }
303
+
304
+ isConnected() {
305
+ return this.connected;
306
+ }
307
+
308
+ async reconnectClient() {
309
+ if (client) {
310
+ try {
311
+ this.adapter.log.info("Reconnecting mqtt client!");
312
+ await client.end();
313
+ client.reconnect();
314
+ } catch (error) {
315
+ this.adapter.catchError(`Failed to reconnect with error: ${error}`, `reconnectClient`);
316
+ }
317
+ }
318
+ }
319
+
320
+ md5hex(str) {
321
+ return crypto.createHash("md5").update(str).digest("hex");
322
+ }
323
+
324
+ md5bin(str) {
325
+ return crypto.createHash("md5").update(str).digest();
326
+ }
327
+
328
+ decryptWithPrivateKey(privateKeyPem, encryptedData) {
329
+ const privateKey = crypto.createPrivateKey({
330
+ key: privateKeyPem,
331
+ format: "pem",
332
+ type: "pkcs8",
333
+ });
334
+
335
+ const decryptedData = crypto.privateDecrypt(
336
+ {
337
+ key: privateKey,
338
+ padding: crypto.constants.RSA_PKCS1_PADDING,
339
+ },
340
+ encryptedData
341
+ );
342
+
343
+ return decryptedData;
344
+ }
345
+ }
346
+
347
+ module.exports = {
348
+ roborock_mqtt_connector,
349
+ };
@@ -0,0 +1,300 @@
1
+
2
+ import sys
3
+
4
+ from mitmproxy.utils import strutils
5
+ from mitmproxy import ctx
6
+ from mitmproxy import tcp
7
+ from websocket import create_connection
8
+ import json
9
+
10
+ import struct
11
+ import binascii
12
+ from struct import unpack
13
+
14
+ ws = create_connection("ws://localhost:7906")
15
+
16
+ class MQTTControlPacket:
17
+ # Packet types
18
+ (
19
+ CONNECT,
20
+ CONNACK,
21
+ PUBLISH,
22
+ PUBACK,
23
+ PUBREC,
24
+ PUBREL,
25
+ PUBCOMP,
26
+ SUBSCRIBE,
27
+ SUBACK,
28
+ UNSUBSCRIBE,
29
+ UNSUBACK,
30
+ PINGREQ,
31
+ PINGRESP,
32
+ DISCONNECT,
33
+ ) = range(1, 15)
34
+
35
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.1_-
36
+ Names = [
37
+ "reserved",
38
+ "CONNECT",
39
+ "CONNACK",
40
+ "PUBLISH",
41
+ "PUBACK",
42
+ "PUBREC",
43
+ "PUBREL",
44
+ "PUBCOMP",
45
+ "SUBSCRIBE",
46
+ "SUBACK",
47
+ "UNSUBSCRIBE",
48
+ "UNSUBACK",
49
+ "PINGREQ",
50
+ "PINGRESP",
51
+ "DISCONNECT",
52
+ "reserved",
53
+ ]
54
+
55
+ PACKETS_WITH_IDENTIFIER = [
56
+ PUBACK,
57
+ PUBREC,
58
+ PUBREL,
59
+ PUBCOMP,
60
+ SUBSCRIBE,
61
+ SUBACK,
62
+ UNSUBSCRIBE,
63
+ UNSUBACK,
64
+ ]
65
+
66
+ def __init__(self, packet):
67
+ self._packet = packet
68
+
69
+ print(sys.executable)
70
+ # Fixed header
71
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718020
72
+ self.packet_type = self._parse_packet_type()
73
+ self.packet_type_human = self.Names[self.packet_type]
74
+ self.dup, self.qos, self.retain = self._parse_flags()
75
+ self.remaining_length = self._parse_remaining_length()
76
+ # Variable header & Payload
77
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718024
78
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718026
79
+ if self.packet_type == self.CONNECT:
80
+ self._parse_connect_variable_headers()
81
+ self._parse_connect_payload()
82
+ elif self.packet_type == self.PUBLISH:
83
+ self._parse_publish_variable_headers()
84
+ self._parse_publish_payload()
85
+ elif self.packet_type == self.SUBSCRIBE:
86
+ self._parse_subscribe_variable_headers()
87
+ self._parse_subscribe_payload()
88
+ elif self.packet_type == self.SUBACK:
89
+ pass
90
+ elif self.packet_type == self.UNSUBSCRIBE:
91
+ pass
92
+ else:
93
+ self.payload = None
94
+
95
+ def pprint(self):
96
+ s = f"[{self.Names[self.packet_type]}]"
97
+
98
+ if self.packet_type == self.CONNECT:
99
+ s += f"""
100
+
101
+ Client Id: {self.payload['ClientId']}
102
+ Will Topic: {self.payload.get('WillTopic')}
103
+ Will Message: {strutils.bytes_to_escaped_str(self.payload.get('WillMessage', b'None'))}
104
+ User Name TEST: {self.payload.get('UserName')}
105
+ Password: {strutils.bytes_to_escaped_str(self.payload.get('Password', b'None'))}
106
+ """
107
+ elif self.packet_type == self.SUBSCRIBE:
108
+ s += " sent topic filters: "
109
+ s += ", ".join([f"'{tf}'" for tf in self.topic_filters])
110
+ elif self.packet_type == self.PUBLISH:
111
+ topic_name = strutils.bytes_to_escaped_str(self.topic_name)
112
+ payload_hex = binascii.hexlify(self.payload).decode()
113
+
114
+ protocol = unpack('>H', self.payload[15:17])[0]
115
+
116
+ if (protocol == 101):
117
+ s += f" '{payload_hex}' to topic '{topic_name}'"
118
+ elif self.packet_type in [self.PINGREQ, self.PINGRESP]:
119
+ pass
120
+ else:
121
+ s = f"Packet type {self.Names[self.packet_type]} is not supported yet!"
122
+
123
+ return s
124
+
125
+ def _parse_length_prefixed_bytes(self, offset):
126
+ field_length_bytes = self._packet[offset : offset + 2]
127
+ field_length = struct.unpack("!H", field_length_bytes)[0]
128
+
129
+ field_content_bytes = self._packet[offset + 2 : offset + 2 + field_length]
130
+
131
+ return field_length + 2, field_content_bytes
132
+
133
+ def _parse_publish_variable_headers(self):
134
+ offset = len(self._packet) - self.remaining_length
135
+
136
+ field_length, field_content_bytes = self._parse_length_prefixed_bytes(offset)
137
+ self.topic_name = field_content_bytes
138
+
139
+ if self.qos in [0x01, 0x02]:
140
+ offset += field_length
141
+ self.packet_identifier = self._packet[offset : offset + 2]
142
+
143
+ def _parse_publish_payload(self):
144
+ fixed_header_length = len(self._packet) - self.remaining_length
145
+ variable_header_length = 2 + len(self.topic_name)
146
+
147
+ if self.qos in [0x01, 0x02]:
148
+ variable_header_length += 2
149
+
150
+ offset = fixed_header_length + variable_header_length
151
+
152
+ self.payload = self._packet[offset:]
153
+
154
+ def _parse_subscribe_variable_headers(self):
155
+ self._parse_packet_identifier()
156
+
157
+ def _parse_subscribe_payload(self):
158
+ offset = len(self._packet) - self.remaining_length + 2
159
+
160
+ self.topic_filters = {}
161
+
162
+ while len(self._packet) - offset > 0:
163
+ field_length, topic_filter_bytes = self._parse_length_prefixed_bytes(offset)
164
+ offset += field_length
165
+
166
+ qos = self._packet[offset : offset + 1]
167
+ offset += 1
168
+
169
+ topic_filter = topic_filter_bytes.decode("utf-8")
170
+ self.topic_filters[topic_filter] = {"qos": qos}
171
+
172
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030
173
+ def _parse_connect_variable_headers(self):
174
+ offset = len(self._packet) - self.remaining_length
175
+
176
+ self.variable_headers = {}
177
+ self.connect_flags = {}
178
+
179
+ self.variable_headers["ProtocolName"] = self._packet[offset : offset + 6]
180
+ self.variable_headers["ProtocolLevel"] = self._packet[offset + 6 : offset + 7]
181
+ self.variable_headers["ConnectFlags"] = self._packet[offset + 7 : offset + 8]
182
+ self.variable_headers["KeepAlive"] = self._packet[offset + 8 : offset + 10]
183
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349229
184
+ self.connect_flags["CleanSession"] = bool(
185
+ self.variable_headers["ConnectFlags"][0] & 0x02
186
+ )
187
+ self.connect_flags["Will"] = bool(
188
+ self.variable_headers["ConnectFlags"][0] & 0x04
189
+ )
190
+ self.will_qos = (self.variable_headers["ConnectFlags"][0] >> 3) & 0x03
191
+ self.connect_flags["WillRetain"] = bool(
192
+ self.variable_headers["ConnectFlags"][0] & 0x20
193
+ )
194
+ self.connect_flags["Password"] = bool(
195
+ self.variable_headers["ConnectFlags"][0] & 0x40
196
+ )
197
+ self.connect_flags["UserName"] = bool(
198
+ self.variable_headers["ConnectFlags"][0] & 0x80
199
+ )
200
+
201
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031
202
+ def _parse_connect_payload(self):
203
+ fields = []
204
+ offset = len(self._packet) - self.remaining_length + 10
205
+
206
+ while len(self._packet) - offset > 0:
207
+ field_length, field_content = self._parse_length_prefixed_bytes(offset)
208
+ fields.append(field_content)
209
+ offset += field_length
210
+
211
+ self.payload = {}
212
+
213
+ for f in fields:
214
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349242
215
+ if "ClientId" not in self.payload:
216
+ self.payload["ClientId"] = f.decode("utf-8")
217
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349243
218
+ elif self.connect_flags["Will"] and "WillTopic" not in self.payload:
219
+ self.payload["WillTopic"] = f.decode("utf-8")
220
+ elif self.connect_flags["Will"] and "WillMessage" not in self.payload:
221
+ self.payload["WillMessage"] = f
222
+ elif self.connect_flags["UserName"] and "UserName" not in self.payload:
223
+ self.payload["UserName"] = f.decode("utf-8")
224
+ elif self.connect_flags["Password"] and "Password" not in self.payload:
225
+ self.payload["Password"] = f
226
+ else:
227
+ raise Exception("")
228
+
229
+ def _parse_packet_type(self):
230
+ return self._packet[0] >> 4
231
+
232
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718022
233
+ def _parse_flags(self):
234
+ dup = None
235
+ qos = None
236
+ retain = None
237
+
238
+ if self.packet_type == self.PUBLISH:
239
+ dup = (self._packet[0] >> 3) & 0x01
240
+ qos = (self._packet[0] >> 1) & 0x03
241
+ retain = self._packet[0] & 0x01
242
+
243
+ return dup, qos, retain
244
+
245
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.4_Size
246
+ def _parse_remaining_length(self):
247
+ multiplier = 1
248
+ value = 0
249
+ i = 1
250
+
251
+ while True:
252
+ encodedByte = self._packet[i]
253
+ value += (encodedByte & 127) * multiplier
254
+ multiplier *= 128
255
+
256
+ if multiplier > 128 * 128 * 128:
257
+ raise Exception("Malformed Remaining Length")
258
+
259
+ if encodedByte & 128 == 0:
260
+ break
261
+
262
+ i += 1
263
+
264
+ return value
265
+
266
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.5_-
267
+ def _parse_packet_identifier(self):
268
+ offset = len(self._packet) - self.remaining_length
269
+ self.packet_identifier = self._packet[offset : offset + 2]
270
+
271
+
272
+
273
+
274
+ def send_to_websocket(command, message):
275
+ if ws.connected:
276
+ data = {
277
+ "command": command,
278
+ "message": message
279
+ }
280
+ ws.send(json.dumps(data))
281
+
282
+
283
+ def tcp_message(flow: tcp.TCPFlow):
284
+ message = flow.messages[-1]
285
+
286
+ mqtt_packet = MQTTControlPacket(message.content)
287
+
288
+ log_message = mqtt_packet.pprint()
289
+ ctx.log.info(log_message)
290
+
291
+ if log_message.startswith("[PUBLISH]") and len(log_message) > len("[PUBLISH] "):
292
+ send_to_websocket("sniffing_decrypt", log_message)
293
+
294
+ # This way we can save topics
295
+ # if mqtt_packet.packet_type == mqtt_packet.PUBLISH:
296
+ # with open("topics.txt", "a") as f:
297
+ # f.write(f"{mqtt_packet.topic_name}\n")
298
+ # elif mqtt_packet.packet_type == mqtt_packet.SUBSCRIBE:
299
+ # with open("topics.txt", "a") as f:
300
+ # f.write(f"{mqtt_packet.topic_filters}\n")