From e15f8410360ed01445837e0e85a06d087312c7d0 Mon Sep 17 00:00:00 2001 From: Marc Date: Sun, 12 Apr 2026 05:40:05 -0600 Subject: [PATCH] fix: remove 302 redirect, serve page directly with embedded token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix (84e0caa) embedded the token in HTML but still did a 302 redirect on first visit. This meant: 1. Phone opens URL with ?token=xxx 2. Server redirects to / (token stripped from URL) 3. redirected request has no token, buildHTML gets undefined 4. Token never embedded in the page Also the old code passed the server's secret 'token' variable instead of the URL parameter 'providedToken' — a security issue. Fix: Remove the redirect entirely. Serve the HTML page directly on first visit with Set-Cookie header (no Location redirect). Pass the actual providedToken from the URL to buildHTML so it gets embedded correctly. The JS includes the token in the WebSocket URL, so auth works even if the cookie isn't available. --- extensions/pi-remote-control/server.ts | 86 +++++++++++++------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/extensions/pi-remote-control/server.ts b/extensions/pi-remote-control/server.ts index 506fe5f..5d81938 100644 --- a/extensions/pi-remote-control/server.ts +++ b/extensions/pi-remote-control/server.ts @@ -103,30 +103,28 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise = {}; + if (!hasValidSession && hasValidToken) { + pruneExpiredSessions(); + const sessionId = generateSessionId(); + validSessions.set(sessionId, Date.now() + SESSION_TTL_MS); + extraHeaders["Set-Cookie"] = `${SESSION_COOKIE}=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`; + } - // Valid session cookie — serve the page - const nonce = randomBytes(16).toString("base64"); - res.writeHead(200, { - "Content-Type": "text/html; charset=utf-8", - "X-Frame-Options": "DENY", - "X-Content-Type-Options": "nosniff", - "Referrer-Policy": "no-referrer", - "Content-Security-Policy": - `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`, - }); - res.end(buildHTML(nonce, token)); + const nonce = randomBytes(16).toString("base64"); + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + "Content-Security-Policy": + `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`, + ...extraHeaders, + }); + res.end(buildHTML(nonce, hasValidToken ? providedToken : undefined)); } else { res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); res.end("Not found"); @@ -329,28 +327,28 @@ export function startServerTailscale(pi: ExtensionAPI, ctx: ExtensionContext): P return; } - if (!hasValidSession && hasValidToken) { - pruneExpiredSessions(); - const sessionId = generateSessionId(); - validSessions.set(sessionId, Date.now() + SESSION_TTL_MS); - res.writeHead(302, { - "Set-Cookie": `${SESSION_COOKIE}=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`, - Location: "/", - }); - res.end(); - return; - } + // If authenticated via token (first visit), register a session cookie but serve the + // page directly — don't redirect. The token is embedded in the HTML so the JS + // can include it in the WebSocket URL. Avoids cookie/redirect issues on mobile. + let extraHeaders: Record = {}; + if (!hasValidSession && hasValidToken) { + pruneExpiredSessions(); + const sessionId = generateSessionId(); + validSessions.set(sessionId, Date.now() + SESSION_TTL_MS); + extraHeaders["Set-Cookie"] = `${SESSION_COOKIE}=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`; + } - const nonce = randomBytes(16).toString("base64"); - res.writeHead(200, { - "Content-Type": "text/html; charset=utf-8", - "X-Frame-Options": "DENY", - "X-Content-Type-Options": "nosniff", - "Referrer-Policy": "no-referrer", - "Content-Security-Policy": - `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`, - }); - res.end(buildHTML(nonce, token)); + const nonce = randomBytes(16).toString("base64"); + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + "Content-Security-Policy": + `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`, + ...extraHeaders, + }); + res.end(buildHTML(nonce, hasValidToken ? providedToken : undefined)); } else { res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); res.end("Not found");