From 7686779fbb75e4a33b3de57517ca2990ea43d63e Mon Sep 17 00:00:00 2001 From: Lopinosaurus Date: Wed, 11 Mar 2026 19:35:15 +0100 Subject: [PATCH] root-me: basic ranking --- logins.txt | 3 ++ public/app.js | 125 +++++++++++++++++++++++++++++++++++++++++++--- public/index.html | 13 +++++ public/style.css | 46 +++++++++++++++++ server.js | 85 ++++++++++++++++++++++++++++--- 5 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 logins.txt diff --git a/logins.txt b/logins.txt new file mode 100644 index 0000000..217342c --- /dev/null +++ b/logins.txt @@ -0,0 +1,3 @@ +724433 +546528 + diff --git a/public/app.js b/public/app.js index b941e96..35f0b27 100644 --- a/public/app.js +++ b/public/app.js @@ -23,6 +23,11 @@ function connectWS() { showAlert(msg.message, msg.html, msg.image); } else if (msg.type === 'dismiss') { hideAlert(); + } else if (msg.type === 'rootme_update') { + renderRootme(msg.ranking); + } else if (msg.type === 'rootme_flag') { + renderRootme(rootmeCache); + showNotif(`FLAG ! ${msg.login} +${msg.gained} PTS — TOTAL : ${msg.newScore} PTS`, true); } }); @@ -64,7 +69,7 @@ const notifBarInner = document.getElementById('notif-bar-inner'); const softAlarmAudio = new Audio('/soft_alarm.mp3'); let notifTimer = null; -function showNotif(message) { +function showNotif(message, sound = true) { notifMessage.textContent = message; // Re-déclencher l'animation de la barre @@ -78,8 +83,10 @@ function showNotif(message) { notifOverlay.classList.remove('hidden'); - softAlarmAudio.currentTime = 0; - softAlarmAudio.play().catch(err => console.error('soft_alarm:', err)); + if (sound) { + softAlarmAudio.currentTime = 0; + softAlarmAudio.play().catch(err => console.error('soft_alarm:', err)); + } if (notifTimer) clearTimeout(notifTimer); notifTimer = setTimeout(() => { @@ -250,9 +257,9 @@ async function loadGeo() { // Premier chargement : on mémorise sans jouer seenGeoLinks = currentLinks; } else { - const hasNew = items.some(i => !seenGeoLinks.has(i.link)); - if (hasNew) { - showNotif('Nouvelle actualité géopolitique'); + const newGeoItems = items.filter(i => !seenGeoLinks.has(i.link)); + if (newGeoItems.length) { + showNotif(`Nouvelle actualité géopolitique : ${newGeoItems[0].title}`, false); seenGeoLinks = currentLinks; } } @@ -279,19 +286,125 @@ async function loadGeo() { } } +// ── Root-me ranking ─────────────────────────────────────────────────────────── + +const rootmeList = document.getElementById('rootme-list'); +const rootmeStatus = document.getElementById('rootme-status'); +let rootmeCache = []; + +function renderRootme(ranking) { + if (!ranking) return; + rootmeCache = ranking; + rootmeList.innerHTML = ''; + + if (!ranking.length) { + rootmeList.innerHTML = '
  • Aucun joueur
  • '; + rootmeStatus.textContent = '—'; + rootmeStatus.className = 'widget-status'; + return; + } + + const rankClass = i => i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : ''; + + ranking.forEach((user, idx) => { + const li = document.createElement('li'); + li.className = 'rootme-item'; + li.innerHTML = ` + #${idx + 1} + ${escapeHtml(user.login)} + ${user.score.toLocaleString('fr-FR')} pts + `; + rootmeList.appendChild(li); + }); + + rootmeStatus.textContent = `${ranking.length} joueur${ranking.length > 1 ? 's' : ''}`; + rootmeStatus.className = 'widget-status ok'; +} + +async function loadRootme() { + rootmeStatus.textContent = '...'; + rootmeStatus.className = 'widget-status'; + try { + const resp = await fetch('/api/rootme'); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.error || `HTTP ${resp.status}`); + } + const ranking = await resp.json(); + renderRootme(ranking); + } catch (err) { + rootmeStatus.textContent = 'ERREUR'; + rootmeStatus.className = 'widget-status err'; + rootmeList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; + } +} + +// ── Carousel ────────────────────────────────────────────────────────────────── + +const CAROUSEL_PAGES = [ + ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-placeholder'], + ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-clock'], +]; +const ALL_CAROUSEL_IDS = [...new Set(CAROUSEL_PAGES.flat())]; +const CAROUSEL_FADE_MS = 450; +let carouselPage = 0; + +function applyCarouselInstant() { + const visible = new Set(CAROUSEL_PAGES[carouselPage]); + ALL_CAROUSEL_IDS.forEach(id => { + const el = document.getElementById(id); + if (!el) return; + el.style.opacity = ''; + el.style.display = visible.has(id) ? '' : 'none'; + }); +} + +function rotateCarousel() { + const prevVisible = new Set(CAROUSEL_PAGES[carouselPage]); + carouselPage = (carouselPage + 1) % CAROUSEL_PAGES.length; + const nextVisible = new Set(CAROUSEL_PAGES[carouselPage]); + + const outgoing = ALL_CAROUSEL_IDS.filter(id => prevVisible.has(id) && !nextVisible.has(id)); + const incoming = ALL_CAROUSEL_IDS.filter(id => !prevVisible.has(id) && nextVisible.has(id)); + + // Fade out + outgoing.forEach(id => { document.getElementById(id).style.opacity = '0'; }); + + setTimeout(() => { + // Hide outgoing, reveal incoming at opacity 0 then fade in + outgoing.forEach(id => { + const el = document.getElementById(id); + el.style.display = 'none'; + el.style.opacity = ''; + }); + incoming.forEach(id => { + const el = document.getElementById(id); + el.style.display = ''; + el.style.opacity = '0'; + el.getBoundingClientRect(); // force reflow + el.style.opacity = ''; + }); + }, CAROUSEL_FADE_MS); +} + // ── Init ───────────────────────────────────────────────────────────────────── function init() { infoHost.textContent = location.host; + applyCarouselInstant(); + setInterval(rotateCarousel, 5 * 60 * 1000); + connectWS(); updateClock(); setInterval(updateClock, 1000); loadAnssi(); loadGeo(); + loadRootme(); setInterval(loadAnssi, 5 * 60 * 1000); setInterval(loadGeo, 5 * 60 * 1000); + // Root-me est mis à jour via WebSocket (rootme_update / rootme_flag) } // ── Helpers ─────────────────────────────────────────────────────────────────── diff --git a/public/index.html b/public/index.html index 9f40921..83fccc8 100644 --- a/public/index.html +++ b/public/index.html @@ -81,6 +81,19 @@ + +
    +
    + ROOT-ME RANKING + ... +
    +
    +
      +
    • Chargement...
    • +
    +
    +
    +
    diff --git a/public/style.css b/public/style.css index bdb5b40..60b635a 100644 --- a/public/style.css +++ b/public/style.css @@ -62,6 +62,13 @@ main.grid { height: calc(100vh - 40px); } +/* ── Carousel grid positions ─────────────────────────────────────────────── */ +#widget-kaspersky { grid-column: 1; grid-row: 1; } +#widget-anssi { grid-column: 2; grid-row: 1; } +#widget-geo { grid-column: 1; grid-row: 2; } +#widget-clock { grid-column: 2; grid-row: 2; } +#widget-placeholder { grid-column: 2; grid-row: 2; } + /* ── Widget ──────────────────────────────────────────────────────────────── */ .widget { display: flex; @@ -70,6 +77,45 @@ main.grid { border: 1px solid var(--border); border-radius: 4px; overflow: hidden; + transition: opacity 0.45s ease; +} + +/* ── Root-me ranking ─────────────────────────────────────────────────────── */ +.rootme-item { + display: grid; + grid-template-columns: 28px 1fr auto; + align-items: center; + gap: 8px; + border-bottom: 1px solid var(--border); + padding: 8px 4px; +} + +.rootme-rank { + color: var(--text-dim); + font-size: 11px; + letter-spacing: 1px; + text-align: right; +} + +.rootme-rank.gold { color: #ffd700; text-shadow: 0 0 6px #ffd700; } +.rootme-rank.silver { color: #c0c0c0; } +.rootme-rank.bronze { color: #cd7f32; } + +.rootme-login { + color: var(--green); + font-size: 13px; + font-weight: bold; + letter-spacing: 1px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rootme-score { + color: var(--green-dim); + font-size: 11px; + letter-spacing: 1px; + white-space: nowrap; } .widget-header { diff --git a/server.js b/server.js index 7daf40f..3a353ac 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const WebSocket = require('ws'); const fetch = require('node-fetch'); const { XMLParser } = require('fast-xml-parser'); const path = require('path'); +const fs = require('fs'); const PORT = process.env.DASHBOARD_PORT || 3000; @@ -125,18 +126,90 @@ app.get('/api/feeds/geo', async (req, res) => { const parser = new XMLParser({ ignoreAttributes: false }); const parsed = parser.parse(xml); const items = parsed?.rss?.channel?.item || []; - const entries = (Array.isArray(items) ? items : [items]).slice(0, 15).map(item => ({ - title: item.title || '', - link: item.link || '', - pubDate: item.pubDate || '', - source: item.source?.['#text'] || item.source || '' - })); + const entries = (Array.isArray(items) ? items : [items]) + .map(item => ({ + title: item.title || '', + link: item.link || '', + pubDate: item.pubDate || '', + source: item.source?.['#text'] || item.source || '' + })) + .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)) + .slice(0, 15); res.json(entries); } catch (err) { res.status(502).json({ error: 'Geo feed fetch failed', detail: err.message }); } }); +// Root-me ranking +const ROOTME_POLL_MS = 10 * 60 * 1000; +let rootmeCache = null; +let rootmePrevScores = {}; // login → last known score + +async function fetchRootmeRanking(apiKey) { + const raw = fs.readFileSync(path.resolve('logins.txt'), 'utf8'); + const ids = raw.split('\n').map(l => l.trim()).filter(Boolean); + const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; + + const results = await Promise.all(ids.map(async id => { + try { + const resp = await fetch( + `https://api.www.root-me.org/auteurs/${id}`, + { headers, timeout: 10000 } + ); + if (resp.status === 429) throw new Error('rate-limited'); + const profile = await resp.json(); + const profileRaw = Array.isArray(profile) ? profile[0] : profile; + const user = profileRaw?.['0'] ?? profileRaw; + if (!user || user.error) return null; + + return { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null }; + } catch (err) { + console.error(`[rootme] fetch error for id "${id}":`, err.message); + return null; + } + })); + + return results.filter(Boolean).sort((a, b) => b.score - a.score); +} + +async function pollRootme() { + const apiKey = process.env.ROOTME_API_KEY; + if (!apiKey) return; + + try { + const ranking = await fetchRootmeRanking(apiKey); + + // Detect score gains and broadcast flag events + const isFirstPoll = Object.keys(rootmePrevScores).length === 0; + if (!isFirstPoll) { + ranking.forEach(user => { + const prev = rootmePrevScores[user.login]; + if (prev !== undefined && user.score > prev) { + const gained = user.score - prev; + console.log(`[rootme] FLAG ! ${user.login} +${gained} pts (${prev} -> ${user.score})`); + broadcast({ type: 'rootme_flag', login: user.login, gained, newScore: user.score }); + } + }); + } + + ranking.forEach(u => { rootmePrevScores[u.login] = u.score; }); + rootmeCache = ranking; + broadcast({ type: 'rootme_update', ranking }); + console.log(`[rootme] polled — ${ranking.length} joueur(s)`); + } catch (err) { + console.error('[rootme] poll error:', err.message); + } +} + +app.get('/api/rootme', (req, res) => { + if (!process.env.ROOTME_API_KEY) return res.status(503).json({ error: 'ROOTME_API_KEY not configured' }); + if (!rootmeCache) return res.json([]); + res.json(rootmeCache); +}); + server.listen(PORT, () => { console.log(`Cyber Dashboard running on http://localhost:${PORT}`); + pollRootme(); + setInterval(pollRootme, ROOTME_POLL_MS); });