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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/config.schema.json +31 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/platform.js +167 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +8 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vacuum_accessory.js +152 -0
- package/dist/vacuum_accessory.js.map +1 -0
- package/package.json +66 -0
- package/roborockLib/data/UserData +4 -0
- package/roborockLib/data/clientID +4 -0
- package/roborockLib/i18n/de/translations.json +188 -0
- package/roborockLib/i18n/en/translations.json +208 -0
- package/roborockLib/i18n/es/translations.json +188 -0
- package/roborockLib/i18n/fr/translations.json +188 -0
- package/roborockLib/i18n/it/translations.json +188 -0
- package/roborockLib/i18n/nl/translations.json +188 -0
- package/roborockLib/i18n/pl/translations.json +188 -0
- package/roborockLib/i18n/pt/translations.json +188 -0
- package/roborockLib/i18n/ru/translations.json +188 -0
- package/roborockLib/i18n/uk/translations.json +188 -0
- package/roborockLib/i18n/zh-cn/translations.json +188 -0
- package/roborockLib/lib/RRMapParser.js +447 -0
- package/roborockLib/lib/deviceFeatures.js +995 -0
- package/roborockLib/lib/localConnector.js +249 -0
- package/roborockLib/lib/map/map.html +110 -0
- package/roborockLib/lib/map/zones.js +713 -0
- package/roborockLib/lib/mapCreator.js +692 -0
- package/roborockLib/lib/message.js +223 -0
- package/roborockLib/lib/messageQueueHandler.js +87 -0
- package/roborockLib/lib/roborockPackageHelper.js +116 -0
- package/roborockLib/lib/roborock_mqtt_connector.js +349 -0
- package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
- package/roborockLib/lib/vacuum.js +636 -0
- package/roborockLib/roborockAPI.js +1365 -0
- package/roborockLib/test.js +31 -0
- 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")
|