homebridge-roborock-vacuum2 1.4.13 → 1.4.15
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 +9 -0
- package/package.json +1 -1
- package/roborockLib/lib/map/zones.js +64 -5
- package/roborockLib/lib/roborockAuth.js +14 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.15
|
|
4
|
+
- Tightened obstacle photo handling in the map UI to accept only base64-encoded image data and render it through browser-generated blob URLs.
|
|
5
|
+
- Added blob URL cleanup when closing or replacing obstacle photos to avoid leaking browser-side object URLs.
|
|
6
|
+
|
|
7
|
+
## 1.4.14
|
|
8
|
+
- Hardened region detection by parsing the configured Roborock host instead of using substring matches.
|
|
9
|
+
- Sanitized map obstacle image URLs before assigning them in the browser UI to reduce XSS and client-side redirect risk.
|
|
10
|
+
- Added explicit read-only permissions to the CI workflow, upgraded GitHub Actions versions, and moved Codecov uploads to a repository secret.
|
|
11
|
+
|
|
3
12
|
## 1.4.13
|
|
4
13
|
- Adjusted `package.json` repository metadata to match the fork URL exactly for npm Trusted Publishing compatibility.
|
|
5
14
|
- Updated the npm publish workflow to use Node 24 and the latest npm CLI for Trusted Publishing compatibility.
|
package/package.json
CHANGED
|
@@ -41,6 +41,63 @@ window.onload = function () {
|
|
|
41
41
|
var triangle = document.getElementById("triangle");
|
|
42
42
|
var largePhoto = document.getElementById("largePhoto");
|
|
43
43
|
var largePhotoImage = document.getElementById("largePhoto-image");
|
|
44
|
+
var dataImagePattern = /^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)$/i;
|
|
45
|
+
|
|
46
|
+
function revokeObjectURL(imageElement) {
|
|
47
|
+
if (imageElement.dataset.objectUrl) {
|
|
48
|
+
URL.revokeObjectURL(imageElement.dataset.objectUrl);
|
|
49
|
+
delete imageElement.dataset.objectUrl;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decodeImageData(rawValue) {
|
|
54
|
+
if (typeof rawValue !== "string") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const value = rawValue.trim();
|
|
59
|
+
if (!value) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const match = value.match(dataImagePattern);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const mimeType = match[1].toLowerCase();
|
|
69
|
+
const base64Payload = match[2].replace(/\s+/g, "");
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const binary = atob(base64Payload);
|
|
73
|
+
const bytes = new Uint8Array(binary.length);
|
|
74
|
+
for (let index = 0; index < binary.length; index++) {
|
|
75
|
+
bytes[index] = binary.charCodeAt(index);
|
|
76
|
+
}
|
|
77
|
+
return new Blob([bytes], { type: mimeType });
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clearImageSource(imageElement) {
|
|
84
|
+
revokeObjectURL(imageElement);
|
|
85
|
+
imageElement.removeAttribute("src");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applySafeImageSource(imageElement, rawValue) {
|
|
89
|
+
const imageBlob = decodeImageData(rawValue);
|
|
90
|
+
if (!imageBlob) {
|
|
91
|
+
console.warn("Ignoring unsafe obstacle image source");
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
revokeObjectURL(imageElement);
|
|
96
|
+
const objectUrl = URL.createObjectURL(imageBlob);
|
|
97
|
+
imageElement.dataset.objectUrl = objectUrl;
|
|
98
|
+
imageElement.src = objectUrl;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
44
101
|
|
|
45
102
|
var socket = new WebSocket("ws://" + window.location.hostname + ":7906");
|
|
46
103
|
|
|
@@ -281,7 +338,7 @@ window.onload = function () {
|
|
|
281
338
|
},
|
|
282
339
|
};
|
|
283
340
|
|
|
284
|
-
|
|
341
|
+
clearImageSource(popupImage);
|
|
285
342
|
|
|
286
343
|
triangle.style.left = "45px";
|
|
287
344
|
triangle.style.top = "100px";
|
|
@@ -292,8 +349,7 @@ window.onload = function () {
|
|
|
292
349
|
socket.onmessage = function (event) {
|
|
293
350
|
const serverData = JSON.parse(event.data);
|
|
294
351
|
|
|
295
|
-
if (serverData.image) {
|
|
296
|
-
popupImage.src = serverData.image;
|
|
352
|
+
if (serverData.image && applySafeImageSource(popupImage, serverData.image)) {
|
|
297
353
|
socket.onmessage = null;
|
|
298
354
|
|
|
299
355
|
popupImage.addEventListener("click", function () {
|
|
@@ -312,12 +368,13 @@ window.onload = function () {
|
|
|
312
368
|
socket.onmessage = function (event) {
|
|
313
369
|
const serverData = JSON.parse(event.data);
|
|
314
370
|
|
|
315
|
-
if (serverData.image) {
|
|
371
|
+
if (serverData.image && applySafeImageSource(largePhotoImage, serverData.image)) {
|
|
316
372
|
popup.style.display = "none";
|
|
373
|
+
clearImageSource(popupImage);
|
|
317
374
|
largePhoto.style.display = "block";
|
|
318
|
-
largePhotoImage.src = serverData.image;
|
|
319
375
|
|
|
320
376
|
largePhotoImage.onclick = function () {
|
|
377
|
+
clearImageSource(largePhotoImage);
|
|
321
378
|
largePhoto.style.display = "none";
|
|
322
379
|
};
|
|
323
380
|
socket.onmessage = null;
|
|
@@ -325,6 +382,7 @@ window.onload = function () {
|
|
|
325
382
|
};
|
|
326
383
|
|
|
327
384
|
setTimeout(() => {
|
|
385
|
+
clearImageSource(largePhotoImage);
|
|
328
386
|
largePhoto.style.display = "none";
|
|
329
387
|
}, 10000);
|
|
330
388
|
});
|
|
@@ -335,6 +393,7 @@ window.onload = function () {
|
|
|
335
393
|
timeoutStart = Date.now();
|
|
336
394
|
popupTimeout = setTimeout(() => {
|
|
337
395
|
popup.style.display = "none";
|
|
396
|
+
clearImageSource(popupImage);
|
|
338
397
|
socket.onmessage = null;
|
|
339
398
|
popupTimeout = null;
|
|
340
399
|
selectedObstacleID = null;
|
|
@@ -22,18 +22,27 @@ function normalizeBaseURL(baseURL) {
|
|
|
22
22
|
return baseURL.replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function parseHostname(baseURL) {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(`https://${normalizeBaseURL(baseURL)}`).hostname.toLowerCase();
|
|
28
|
+
} catch {
|
|
29
|
+
return normalizeBaseURL(baseURL).split("/")[0].split(":")[0].toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
function getRegionConfig(baseURL) {
|
|
26
|
-
const
|
|
27
|
-
|
|
34
|
+
const hostname = parseHostname(baseURL);
|
|
35
|
+
const labels = hostname.split(".").filter(Boolean);
|
|
36
|
+
if (labels.includes("euiot")) {
|
|
28
37
|
return { country: "DE", countryCode: "49" };
|
|
29
38
|
}
|
|
30
|
-
if (
|
|
39
|
+
if (labels.includes("usiot")) {
|
|
31
40
|
return { country: "US", countryCode: "1" };
|
|
32
41
|
}
|
|
33
|
-
if (
|
|
42
|
+
if (labels.includes("cniot")) {
|
|
34
43
|
return { country: "CN", countryCode: "86" };
|
|
35
44
|
}
|
|
36
|
-
if (
|
|
45
|
+
if (hostname === "api.roborock.com") {
|
|
37
46
|
return { country: "SG", countryCode: "65" };
|
|
38
47
|
}
|
|
39
48
|
return { country: "US", countryCode: "1" };
|