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 { 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 BASE = '/api/gitea/_web';
|
||||||
const ORIGINS = ['http://localhost:3000', 'https://gitea.officer.dev'];
|
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', () => {
|
describe('the origin Gitea declares about itself', () => {
|
||||||
test('is discovered from window.config, so ROOT_URL needs no second setting', () => {
|
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
|
// 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;
|
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.
|
* 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.
|
// attribute pass then skips as already-prefixed.
|
||||||
const declared = appUrlOrigin(html);
|
const declared = appUrlOrigin(html);
|
||||||
const all = declared ? [...origins, declared] : origins;
|
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