mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(browser): stop malformed localhost pages from freezing the new tab page probe (#34427)
The title and icon-link regexes rescanned the rest of the body from every unclosed opener, so a 192 KB page of repeated '<link ' or '<title>' blocked the main or runtime event loop for seconds. Scan tags forward only.
This commit is contained in:
@@ -59,6 +59,22 @@ describe('probeWorkspacePortPage', () => {
|
||||
).resolves.toMatchObject({ kind: 'page', title: 'Sign in', faviconDataUrl: null })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['unclosed links', `<title>ok</title>${'<link '.repeat(32_000)}`, 'ok'],
|
||||
['unclosed titles', '<title>'.repeat(27_000), null]
|
||||
])('parses a page of %s without stalling the event loop', async (_name, page, title) => {
|
||||
const port = await serve((_request, response) => {
|
||||
response.writeHead(200, { 'content-type': 'text/html' }).end(page)
|
||||
})
|
||||
|
||||
const startedAt = performance.now()
|
||||
await expect(
|
||||
probeWorkspacePortPage({ host: 'localhost', port, protocol: 'http' })
|
||||
).resolves.toEqual({ kind: 'page', title, faviconDataUrl: null })
|
||||
// Rescanning the body from each unclosed opener took seconds on this ~190 KB page.
|
||||
expect(performance.now() - startedAt).toBeLessThan(500)
|
||||
})
|
||||
|
||||
it('leaves out APIs and error pages', async () => {
|
||||
const api = await serve((_request, response) => {
|
||||
response.writeHead(200, { 'content-type': 'application/json' }).end('{"ok":true}')
|
||||
|
||||
@@ -110,9 +110,26 @@ function fetchLoopback(url: URL, maxBytes: number): Promise<ProbeResponse | null
|
||||
})
|
||||
}
|
||||
|
||||
// Tag scans only move forward: rescanning from every unclosed opener is quadratic on
|
||||
// a malformed page, and this parse blocks the main or runtime event loop.
|
||||
function extractHtmlTitle(html: string): string | null {
|
||||
const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html)
|
||||
const title = match ? decodeHtmlEntities(match[1]).replace(/\s+/g, ' ').trim() : ''
|
||||
const open = /<title\b/gi
|
||||
if (!open.test(html)) {
|
||||
return null
|
||||
}
|
||||
const tagEnd = html.indexOf('>', open.lastIndex)
|
||||
if (tagEnd === -1) {
|
||||
return null
|
||||
}
|
||||
const close = /<\/title>/gi
|
||||
close.lastIndex = tagEnd + 1
|
||||
const end = close.exec(html)
|
||||
if (!end) {
|
||||
return null
|
||||
}
|
||||
const title = decodeHtmlEntities(html.slice(tagEnd + 1, end.index))
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return title ? title.slice(0, 120) : null
|
||||
}
|
||||
|
||||
@@ -133,7 +150,14 @@ function decodeHtmlEntities(text: string): string {
|
||||
}
|
||||
|
||||
function findIconHref(html: string): string | null {
|
||||
for (const tag of html.match(/<link\b[^>]*>/gi) ?? []) {
|
||||
const open = /<link\b/gi
|
||||
while (open.test(html)) {
|
||||
const end = html.indexOf('>', open.lastIndex)
|
||||
if (end === -1) {
|
||||
return null
|
||||
}
|
||||
const tag = html.slice(open.lastIndex, end)
|
||||
open.lastIndex = end + 1
|
||||
const attributes = new Map<string, string>()
|
||||
for (const [, name, double, single, bare] of tag.matchAll(
|
||||
/\b(rel|href)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi
|
||||
|
||||
Reference in New Issue
Block a user