all repos — red-anatomia @ 43cc40161c75c7c9bb5bef2c1ce2d551b7f24484

red-anatomia

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();
  }
}