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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-vacuum2",
3
- "version": "1.4.13",
3
+ "version": "1.4.15",
4
4
  "description": "Roborock Vacuum Cleaner - plugin for Homebridge.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -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
- if (popupImage.src) popupImage.src = "";
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 lower = normalizeBaseURL(baseURL).toLowerCase();
27
- if (lower.includes("euiot")) {
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 (lower.includes("usiot")) {
39
+ if (labels.includes("usiot")) {
31
40
  return { country: "US", countryCode: "1" };
32
41
  }
33
- if (lower.includes("cniot")) {
42
+ if (labels.includes("cniot")) {
34
43
  return { country: "CN", countryCode: "86" };
35
44
  }
36
- if (lower.includes("api.roborock.com")) {
45
+ if (hostname === "api.roborock.com") {
37
46
  return { country: "SG", countryCode: "65" };
38
47
  }
39
48
  return { country: "US", countryCode: "1" };