From 47e3983f7eef91b91002f4431c595e629c602e19 Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 09:28:04 +0000 Subject: [PATCH] rewrite asset paths baked into the page, including JSON-escaped ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea inlines a script naming its own worker: '\/assets\/js\/eventsource.sharedworker.…js'. That is not an attribute and its slashes are backslash-escaped, so neither the attribute rewrite nor appSubUrl saw it. Unprefixed, the browser resolved it against the PLATFORM's origin, got the SPA's index.html, and reported "Uncaught SyntaxError: Unexpected token '<'" from a file that appeared to load — along with a dead SharedWorker and a heatmap parsing HTML as JSON. appSubUrl covers everything Gitea's code BUILDS at runtime. This covers what is already in the page before that code runs. Narrow on purpose, like dev-proxy's /_bun/ rule: a blanket rewrite of absolute paths inside JavaScript corrupts strings that are not URLs. /assets/ is Gitea's own asset root and specific enough to match safely. The escaping has to match the string it lands in — \/api\/gitea\/_web, not \\/api — or the JS stops parsing. Both forms are tested against a live sample. --- sidecar/rewrite.test.ts | 31 ++++++++++++++++++++++++++++++- sidecar/rewrite.ts | 25 ++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/sidecar/rewrite.test.ts b/sidecar/rewrite.test.ts index 79894e2..68f7610 100644 --- a/sidecar/rewrite.test.ts +++ b/sidecar/rewrite.test.ts @@ -1,5 +1,12 @@ import { describe, expect, test } from 'bun:test'; -import { rewriteHtml, rewriteLocation, rewriteSetCookie, rewriteAttributes, rewriteAppSubUrl } from './rewrite'; +import { + rewriteHtml, + rewriteLocation, + rewriteSetCookie, + rewriteAttributes, + rewriteAppSubUrl, + rewriteAssetPaths, +} from './rewrite'; const BASE = '/api/gitea/_web'; const ORIGINS = ['http://localhost:3000', 'https://gitea.officer.dev']; @@ -90,6 +97,28 @@ describe('redirects — the most visible failure, because the frame just leaves' }); }); +describe('asset paths baked into the page before Gitea\'s own code runs', () => { + test('the JSON-escaped form — the SharedWorker URL that broke the dashboard', () => { + // Verbatim from a live page. Not an attribute, and its slashes are backslash-escaped, so neither the + // attribute rewrite nor appSubUrl touches it. Unprefixed, the browser resolves it against the + // PLATFORM's origin, gets the SPA's index.html, and reports `Unexpected token '<'` from a file that + // looks like it loaded. + const real = String.raw`'\/assets\/js\/eventsource.sharedworker.DV1Z2uJv.js',`; + const out = rewriteAssetPaths(real, BASE); + expect(out).toContain(String.raw`'\/api\/gitea\/_web\/assets\/js\/eventsource`); + // The escaping has to match the string it lands in, or the JS stops parsing. + expect(out).not.toContain(String.raw`\\/api`); + }); + + test('the plain quoted form too', () => { + expect(rewriteAssetPaths(`x = "/assets/img/logo.svg"`, BASE)).toContain(`"${BASE}/assets/img/logo.svg"`); + }); + + test('leaves a path that is not an asset root alone', () => { + expect(rewriteAssetPaths(`x = "/user/login"`, BASE)).toBe(`x = "/user/login"`); + }); +}); + describe('the origin Gitea declares about itself', () => { test('is discovered from window.config, so ROOT_URL needs no second setting', () => { // The sidecar only knows where it FETCHES from — usually loopback. The public origin lives in the diff --git a/sidecar/rewrite.ts b/sidecar/rewrite.ts index ea6ec8b..3ebb75b 100644 --- a/sidecar/rewrite.ts +++ b/sidecar/rewrite.ts @@ -103,6 +103,29 @@ export function rewriteAbsolute(html: string, base: string, origins: string[]): return out; } +/** + * Asset paths embedded in JavaScript, including JSON-escaped ones. + * + * Gitea inlines a script that names its own worker: `'\\/assets\\/js\\/eventsource.sharedworker.…js'`. + * That is not an attribute and its slashes are backslash-escaped, so neither of the rewrites above sees + * it. Unprefixed, the browser resolves it against the PLATFORM's origin, gets the SPA's index.html, and + * reports `Uncaught SyntaxError: Unexpected token '<'` from a file that looks like it loaded. + * + * NARROW on purpose, like dev-proxy's `/_bun/` rule. A blanket rewrite of absolute paths inside + * JavaScript corrupts strings that are not URLs; `/assets/` is Gitea's own asset root and specific + * enough to match safely. + * + * `appSubUrl` covers everything Gitea's code BUILDS at runtime. This covers what is already baked into + * the page before that code runs. + */ +export function rewriteAssetPaths(html: string, base: string): string { + // `/api/gitea/_web` becomes `\/api\/gitea\/_web`, matching the escaping of the string it is going into. + const escapedBase = base.split('/').join('\\/'); + return html + .replace(/\\\/assets\\\//g, `${escapedBase}\\/assets\\/`) + .replace(/(["'`])\/assets\//g, (_m, q: string) => `${q}${base}/assets/`); +} + /** * The public URL Gitea believes it has, read out of its own page. * @@ -128,7 +151,7 @@ export function rewriteHtml(html: string, base: string, origins: string[]): stri // attribute pass then skips as already-prefixed. const declared = appUrlOrigin(html); const all = declared ? [...origins, declared] : origins; - return rewriteAttributes(rewriteAppSubUrl(rewriteAbsolute(html, base, all), base), base); + return rewriteAssetPaths(rewriteAttributes(rewriteAppSubUrl(rewriteAbsolute(html, base, all), base), base), base); } /**