src/collectors/browser.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
import type { Observations, SinaisBrowser } from '../core/types.js';
import { coletarLighthouse } from './lighthouse.js';
import { carregarDataset } from '../detection/dataset.js';
function chavesJsDoDataset(): string[] {
const chaves = new Set<string>();
for (const fp of Object.values(carregarDataset().tecnologias)) {
for (const k of Object.keys(fp.js ?? {})) chaves.add(k);
}
return [...chaves];
}
export interface OpcoesBrowser {
lighthouse: boolean;
progresso: (etapa: string) => void;
}
export async function coletarBrowser(obs: Observations, opcoes: OpcoesBrowser): Promise<void> {
const { chromium } = await import('playwright');
const sinais: SinaisBrowser = {
requisicoes: [],
apis: [],
websockets: [],
serviceWorkers: [],
globaisJs: [],
errosConsole: [],
dominiosTerceiros: [],
};
const browser = await chromium.launch({ headless: true });
const contexto = await browser.newContext({
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0 Safari/537.36',
});
const pagina = await contexto.newPage();
let hostAlvo = '';
try {
hostAlvo = new URL(obs.urlFinal || obs.urlSolicitada).hostname.replace(/^www\./, '');
} catch {
/* ignore */
}
const terceiros = new Set<string>();
pagina.on('request', (req) => {
const tipo = req.resourceType();
sinais.requisicoes.push({ url: req.url(), tipo });
if (tipo === 'xhr' || tipo === 'fetch') sinais.apis.push(req.url());
try {
const h = new URL(req.url()).hostname.replace(/^www\./, '');
if (h && h !== hostAlvo && !h.endsWith('.' + hostAlvo)) terceiros.add(h);
} catch {
/* ignore */
}
});
pagina.on('websocket', (ws) => sinais.websockets.push(ws.url()));
pagina.on('console', (msg) => {
if (msg.type() === 'error') sinais.errosConsole.push(msg.text().slice(0, 200));
});
try {
const resp = await pagina.goto(obs.urlFinal || obs.urlSolicitada, {
waitUntil: 'networkidle',
timeout: 30000,
});
if (resp) obs.statusFinal = resp.status();
obs.html = await pagina.content();
const scriptsCarregados = sinais.requisicoes
.filter((r) => r.tipo === 'script')
.map((r) => r.url);
obs.scriptSrc = Array.from(new Set([...obs.scriptSrc, ...scriptsCarregados]));
sinais.globaisJs = await pagina.evaluate((chaves: string[]) => {
const resolvidas: string[] = [];
for (const chave of chaves) {
try {
let alvo: unknown = globalThis;
let achou = true;
for (const parte of chave.split('.')) {
if (alvo == null) {
achou = false;
break;
}
alvo = (alvo as Record<string, unknown>)[parte];
}
if (achou && alvo !== undefined) resolvidas.push(chave);
} catch {
/* propriedade inacessÃvel */
}
}
return resolvidas;
}, chavesJsDoDataset());
obs.globaisJs = sinais.globaisJs;
sinais.serviceWorkers = contexto.serviceWorkers().map((w) => w.url());
sinais.dominiosTerceiros = [...terceiros].sort();
obs.browser = sinais;
if (opcoes.lighthouse) {
opcoes.progresso('auditando com Lighthouse');
await coletarLighthouse(obs);
}
} catch (erro) {
obs.avisos.push(`Falha no modo completo: ${(erro as Error).message}`);
} finally {
await browser.close();
}
}
|