diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52f7b5f..2d57ba7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,10 @@ jobs: # de formatos entre proveedores y el analizador del back. Los que levantan # salas o contenedores quedan fuera — necesitan Docker y varios minutos. demos: - runs-on: ubuntu-latest + # Fijo y no ubuntu-latest: cuando latest pasó a 26.04, Playwright 1.56.1 + # (el de docker/qa) ya no pudo instalar Chromium ahí y el job se cayó antes + # de la demo de ver_preview. Se sube cuando se suba Playwright. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -57,6 +60,7 @@ jobs: - run: npm run demo:ver-preview - run: npm run demo:internet - run: npm run demo:publicar + - run: npm run demo:proxy-rutas - run: npm run demo:documento - run: npm run demo:doc-sync - run: npm run demo:comentarios diff --git a/server/src/demos/proxy-rutas.ts b/server/src/demos/proxy-rutas.ts index 28f5274..85f46b9 100644 --- a/server/src/demos/proxy-rutas.ts +++ b/server/src/demos/proxy-rutas.ts @@ -130,6 +130,25 @@ function main() { check("un módulo sin cookie ni referer", aLaSala("/src/main.tsx", {})); } + console.log("\n6b. Una app que navega a su inicio se queda en el preview"); + { + /** + * El bug que cubre: un link "Inicio" (href="/") dentro de la app pedía la + * raíz desde el iframe, y se le servía la Sala entera: el chat de Multti + * aparecía dentro de la vista previa hasta que alguien recargaba. + */ + const desdeElIframe = { ...COOKIE, dest: "iframe" }; + check("la raíz pedida desde el iframe va al preview", alPreview("/", desdeElIframe)); + check("con query también", alPreview("/?pagina=2", desdeElIframe)); + check("index.html desde el iframe también", alPreview("/index.html", desdeElIframe)); + check("con el referer del preview basta, sin cookie", alPreview("/", { ...REFERER, dest: "iframe" })); + check("y conserva la ruta tal cual", parsePreviewUrl("/?pagina=2", desdeElIframe)?.rest === "/?pagina=2"); + check("la raíz abierta como página sigue siendo la Sala", aLaSala("/", { ...COOKIE, dest: "document" })); + check("sin cabecera, sigue siendo la Sala", aLaSala("/")); + check("desde un iframe sin sala identificada, la Sala", aLaSala("/", { dest: "iframe" })); + check("lo de la Sala no cambia aunque venga de un iframe", aLaSala("/socket.io/?EIO=4", desdeElIframe)); + } + console.log("\n7. La página de la sala se distingue de sus archivos"); { /** diff --git a/server/src/engine/proxy.ts b/server/src/engine/proxy.ts index 09fd0fd..62b63b8 100644 --- a/server/src/engine/proxy.ts +++ b/server/src/engine/proxy.ts @@ -82,6 +82,11 @@ const ROOM_COOKIE = "multi_room"; */ const ESPERAS = [250, 500, 1000, 2000]; +/** Una cabecera que Node puede dar como lista, tomada como texto. */ +function headerUnico(v: string | string[] | undefined): string | undefined { + return Array.isArray(v) ? v[0] : v; +} + /** Lee el roomId de la cookie (si viene). */ function roomFromCookie(cookieHeader?: string): string | null { if (!cookieHeader) return null; @@ -100,7 +105,7 @@ function roomFromCookie(cookieHeader?: string): string | null { */ export function parsePreviewUrl( url: string, - headers: { referer?: string; cookie?: string }, + headers: { referer?: string; cookie?: string; dest?: string }, ): { roomId: string; rest: string } | null { // Caso 1: URL explícita bajo /preview/:roomId/... if (url.startsWith(PREFIX)) { @@ -112,15 +117,25 @@ export function parsePreviewUrl( return { roomId, rest }; } - // La raíz SIEMPRE es la Sala. La cookie del preview sobrevive en la pestaña, - // así que sin esto abrir Multi después de ver un preview te daría la app del - // proyecto en lugar de la Sala. const soloRuta = url.split("?")[0]; - if (soloRuta === "/") return null; + const fromReferer = headers.referer?.match(/\/preview\/([a-z0-9-]{1,64})(?:\/|$)/i)?.[1]; - if (RUTAS_DE_LA_SALA.some((p) => soloRuta === p || soloRuta.startsWith(p))) return null; + // La raíz es la Sala, salvo que la pida el iframe del preview. + // + // La cookie del preview sobrevive en la pestaña, así que sin la primera regla + // abrir Multti después de ver un preview te daría la app del proyecto en vez + // de la Sala. Pero una app que navega a su inicio (un link "Inicio", el logo, + // un router que redirige a "/") pide la raíz DESDE el iframe, y ahí se le + // servía la Sala entera: el chat de Multti aparecía dentro de la vista previa + // hasta que alguien recargaba. El navegador dice quién pide con + // Sec-Fetch-Dest, y la Sala nunca se abre dentro de un iframe. + if (soloRuta === "/" || soloRuta === "/index.html") { + if (headers.dest !== "iframe") return null; + const roomId = fromReferer ?? roomFromCookie(headers.cookie); + return roomId ? { roomId, rest: url } : null; + } - const fromReferer = headers.referer?.match(/\/preview\/([a-z0-9-]{1,64})(?:\/|$)/i)?.[1]; + if (RUTAS_DE_LA_SALA.some((p) => soloRuta === p || soloRuta.startsWith(p))) return null; // `/assets/` lo usan los dos: solo va al preview si la petición viene de ahí. if (soloRuta.startsWith(RUTA_COMPARTIDA) && !fromReferer) return null; @@ -175,6 +190,7 @@ export function handlePreviewRequest( const parsed = parsePreviewUrl(req.url ?? "", { referer: req.headers.referer, cookie: req.headers.cookie, + dest: headerUnico(req.headers["sec-fetch-dest"]), }); if (D) console.log(`[proxy] IN url=${req.url} parsed=${JSON.stringify(parsed)}`); if (!parsed) return false;