Egosurfing: búsqueda real de huella digital con resultados top 5

Backend:
- Nuevo route GET /api/egosearch con rate limit (8 req/min)
- Usa Google Custom Search API si GOOGLE_API_KEY+CSE_ID configurados
- Fallback a instancias públicas SearXNG con JSON API (sin API key)
- Devuelve top 5: title, url, snippet, domain, engine

Frontend egosurfing.html:
- Barra de búsqueda prominente con 5 modos (nombre/email/usuario/teléfono/libre)
- Resultados en cards: dominio, título, snippet, acciones (ver, RTBF, GDPR)
- RTBF link contextual según el dominio del resultado
- Google dorking rápido: plantillas con 1 clic que se lanzan al buscador
- Herramientas complementarias: HIBP, TinEye, WhatsMyName, formularios RTBF

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hacklab 2026-04-07 12:52:35 +02:00
parent 7507b3b8e3
commit f1d80dde13
4 changed files with 728 additions and 392 deletions

107
api/routes/egosearch.js Normal file
View file

@ -0,0 +1,107 @@
'use strict';
/* ── Instancias públicas SearXNG con JSON API (fallback si no hay Google CSE) ── */
const SEARX_INSTANCES = [
'https://searx.be',
'https://priv.au',
'https://search.mdosch.de',
'https://searxng.site',
];
async function searchSearX(instance, query) {
const url = `${instance}/search?` + new URLSearchParams({
q: query,
format: 'json',
categories: 'general',
language: 'es-ES',
});
const res = await fetch(url, {
headers: {
'User-Agent': 'resetea.net/1.0 (egosurfing privacy tool)',
'Accept': 'application/json',
},
signal: AbortSignal.timeout(7000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return (data.results || []).map(r => ({
title: r.title || '',
url: r.url || '',
snippet: r.content || '',
engine: r.engine || 'web',
}));
}
async function searchGoogle(query) {
const url = 'https://www.googleapis.com/customsearch/v1?' + new URLSearchParams({
key: process.env.GOOGLE_API_KEY,
cx: process.env.GOOGLE_CSE_ID,
q: query,
num: 10,
hl: 'es',
});
const res = await fetch(url, { signal: AbortSignal.timeout(7000) });
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const data = await res.json();
return (data.items || []).map(item => ({
title: item.title || '',
url: item.link || '',
snippet: item.snippet || '',
engine: 'google',
}));
}
function domainOf(rawUrl) {
try { return new URL(rawUrl).hostname.replace(/^www\./, ''); }
catch { return ''; }
}
module.exports = async (req, res) => {
const { q } = req.query;
if (!q || q.trim().length < 2)
return res.status(400).json({ error: 'Introduce al menos 2 caracteres.' });
if (q.trim().length > 300)
return res.status(400).json({ error: 'Búsqueda demasiado larga.' });
/* Si la query no tiene operadores especiales la ponemos entre comillas
para forzar coincidencia exacta (ideal para nombre/email/alias). */
const query = /[:"()]/.test(q) ? q.trim() : `"${q.trim()}"`;
let raw = [];
try {
if (process.env.GOOGLE_API_KEY && process.env.GOOGLE_CSE_ID) {
raw = await searchGoogle(query);
} else {
for (const inst of SEARX_INSTANCES) {
try {
raw = await searchSearX(inst, query);
if (raw.length) break;
} catch (e) {
console.warn(`[egosearch] ${inst} falló:`, e.message);
}
}
}
} catch (e) {
console.error('[egosearch] error final:', e.message);
return res.status(502).json({ error: 'El servicio de búsqueda no está disponible ahora. Inténtalo de nuevo.' });
}
const results = raw.slice(0, 5).map(r => ({
title: r.title,
url: r.url,
snippet: r.snippet,
domain: domainOf(r.url),
engine: r.engine,
}));
res.json({ results, query, total: raw.length });
};