rewrite asset paths baked into the page, including JSON-escaped ones
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.
This commit is contained in:
+30
-1
@@ -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
|
||||
|
||||
+24
-1
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user