Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions flasher/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,22 @@ const terminal = {
function post(msg: OutboundMessage): void {
// targetOrigin narrows from '*' to the opener's real origin once known; see
// its declaration. Outbound frames carry no nonce (see protocol.ts).
opener?.postMessage(msg, targetOrigin);
try {
opener?.postMessage(msg, targetOrigin);
} catch (err) {
// A malformed origin= hash param (e.g. origin=null) makes postMessage throw,
// which would wedge the ready handshake. Fall back to '*' so frames keep
// flowing; outbound frames carry no nonce (see protocol.ts), so the broader
// audience leaks nothing. Log rather than swallow so an unrelated failure
// (not just a bad origin) is still visible.
console.error("Flasher postMessage failed; falling back to '*':", err);
targetOrigin = "*";
try {
opener?.postMessage(msg, "*");
} catch (err2) {
console.error("Flasher postMessage failed after origin fallback:", err2);
}
}
}

function setState(state: FlashState, detail: string): void {
Expand Down Expand Up @@ -167,6 +182,9 @@ window.addEventListener("message", (ev: MessageEvent) => {
if (!data || data.type !== "esphome-web-flash:firmware") return;
if (data.nonce !== nonce) return;
if (!isFlashParts(data.parts)) {
// The opener has attached and sent, so stop re-announcing ready even though
// the payload is unusable, mirroring the accepted path below.
stopReadyRetry();
setState("error", "Received a malformed firmware payload.");
return;
}
Expand Down Expand Up @@ -504,7 +522,7 @@ installBtn.addEventListener("click", async () => {
data: new Uint8Array(p.data),
address: p.address,
}));
await runFlash(files, firmware.erase ?? true);
await runFlash(files, firmware.erase !== false);
return;
}
const file = fileInput.files?.[0];
Expand Down
32 changes: 32 additions & 0 deletions flasher/test/handshake.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ try {
else if (enabledAfterBad) fail("install enabled after malformed payload");
else console.log("PASS: malformed payload rejected with error state");

// 2b-ii. ...and the malformed payload stops the ready re-announce: the opener
// has clearly attached, so it must not keep receiving ready frames.
await a.evaluate(() => {
window.__msgs.length = 0;
});
await new Promise((r) => setTimeout(r, 700));
const readyAfterBad = (await a.evaluate(() => window.__msgs)).some(
(m) => m && m.type === "esphome-web-flash:ready",
);
if (readyAfterBad) fail("ready still re-announced after malformed payload");
else console.log("PASS: ready retry stopped after malformed payload");

// 2c. a negative/non-integer address is rejected at the boundary
await a.evaluate(() => {
window.__b.postMessage(
Expand All @@ -132,6 +144,26 @@ try {
fail("negative address not rejected: " + label);
else console.log("PASS: negative address rejected at boundary");

// 2d. a malformed origin= hash param must not wedge the ready handshake:
// postMessage to a bad targetOrigin throws, and the receiver falls back to '*'.
{
const c = await browser.newPage();
await c.goto(`${base}/opener.html`);
const [pop2] = await Promise.all([
new Promise((res) => c.once("popup", res)),
c.evaluate((u) => window.__open(u), `${base}/#nonce=n2&origin=null`),
]);
await pop2.waitForNetworkIdle({ idleTime: 300 }).catch(() => {});
await new Promise((r) => setTimeout(r, 300));
const ready2 = (await c.evaluate(() => window.__msgs)).find(
(m) => m && m.type === "esphome-web-flash:ready",
);
if (!ready2) fail("ready not received when origin= hash param is malformed");
else console.log("PASS: malformed origin falls back to '*', ready still sent");
await pop2.close();
await c.close();
}

// 3. correct nonce accepted -> button enabled, state mirrored back
await a.evaluate(() => {
window.__b.postMessage(
Expand Down
Loading