Ctrl K
ComunidadGuías…

server.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
'use strict';
// ============================================================
//  InfraNet Pro - Server Node.js
//  Avvio:  node server.js
//  Apri:   http://localhost:8421
//
//  Struttura:
//    server.js          -> Express app, route CRUD progetti, route /api/poll
//    auth.js            -> autenticazione, sessioni, gestione utenti
//    drivers/snmp.js    -> driver SNMP v1/v2c/v3
//    drivers/<name>.js  -> futuri driver (Cisco RESTCONF, Aruba Central...)
// ============================================================

const express   = require('express');
const path      = require('path');
const auth      = require('./auth');

const PORT         = parseInt(process.env.PORT || '8421', 10);
// Interfaccia di bind. Default 127.0.0.1 (solo loopback — non esposto in rete; vedi README).
// In container si imposta HOST=0.0.0.0 e si pubblica la porta SOLO su 127.0.0.1 dell'host.
const HOST         = process.env.HOST || '127.0.0.1';
const ROOT         = __dirname;

// ---- Persistenza progetti (server/projects-store.js) ------------------------
const { PROJECTS_DIR, hardenTree, hardenFile, hardenBackup } = require('./server/projects-store');

// ============================================================
// Express
// ============================================================

const app = express();

app.use(express.json({ limit: '20mb' }));

// SEC-M3 (audit 2026-07-21): header di sicurezza di base su OGNI risposta.
// - nosniff: niente MIME-sniffing (blocca alcuni XSS via content-type confuso).
// - frame-ancestors 'none' + X-Frame-Options DENY: niente framing → anti-clickjacking.
// - CSP: default-src 'self' con object-src/base-uri stretti. script/style tengono
//   'unsafe-inline' PERCHÉ l'app usa handler ed elementi inline (ASSE B non chiuso) e
//   stili inline nei template. NIENTE 'unsafe-eval': c'era, «per librerie che compilano a
//   runtime», ma il frontend non ha dipendenze e l'unico `new Function` era codice morto
//   (rimosso). Lo guardano test/security-headers.test.js (la politica mandata davvero) e
//   le regole no-eval/no-new-func di eslint.config.js (il sorgente). Tutte
//   le risorse sono self-hosted (Font Awesome locale, nessun CDN) e le chiamate LLM sono
//   server-side → connect-src 'self'. img data:/blob: per bgImage base64 e SVG skin.
const CSP = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline'",
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' data: blob:",
  "font-src 'self'",
  "connect-src 'self'",
  "object-src 'none'",
  "base-uri 'self'",
  "form-action 'self'",
  "frame-ancestors 'none'",
].join('; ');
app.use((_, res, next) => {
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('X-Frame-Options', 'DENY');
  res.set('Referrer-Policy', 'no-referrer');
  res.set('Content-Security-Policy', CSP);
  next();
});

// Logging
app.use((req, res, next) => {
  const ts = new Date().toTimeString().substring(0, 8);
  console.log(`  [${ts}] ${req.method} ${req.path}`);
  next();
});

// no-cache sugli asset del frontend (HTML/CSS/JS/lib): il browser DEVE rivalidare
// col server prima di riusare la copia in cache -> un file cambiato su disco viene
// sempre riservito fresco (304 se invariato). Chiude la classe di bug "ho corretto
// il CSS/HTML ma il browser mostra ancora la versione vecchia dalla cache" (modali
// tagliati, reskin...). I woff2 di Font Awesome NON sono qui -> restano cacheabili.
app.use((req, res, next) => {
  if (req.method === 'GET' && (
      req.path === '/' || req.path === '/export.js' ||
      req.path.startsWith('/styles/') || req.path.startsWith('/dist/') || req.path.startsWith('/lib/')
  )) res.set('Cache-Control', 'no-cache');
  next();
});

// no-store su OGNI risposta /api/*: i JSON dell'API portano dati del progetto,
// config mascherate, l'identità dell'utente (/api/auth/me). Vietarne del tutto la
// conservazione (cache del browser, disco, proxy condiviso) evita che restino
// leggibili dopo il logout o su una macchina condivisa. Vale per ogni metodo, non
// solo GET; gli asset del frontend (sopra) restano rivalidabili con no-cache.
app.use((req, res, next) => {
  if (req.path.startsWith('/api/')) res.set('Cache-Control', 'no-store');
  next();
});

// ---- Frontend statico -------------------------------------------------------

// CSS modularizzato (cartella styles/): partial ordinate, caricate via <link>
// in netmapper.html. Solo .css, niente traversal.
app.get('/styles/:file', (req, res) => {
  if (!/^[a-zA-Z0-9_-]+\.css$/.test(req.params.file)) return res.status(404).end();
  res.sendFile(path.join(ROOT, 'styles', req.params.file));
});
// (route storica /app.js RIMOSSA: il monolite pre-ESM non esiste piu al root, il
//  frontend carica /dist/app.bundle.js; una GET /app.js cadeva su un sendFile ENOENT
//  che serviva la pagina d'errore HTML di Express col path assoluto del server nel
//  body. Ora cade nel 404 catch-all JSON pulito, senza info-disclosure.)
app.get('/export.js', (_, res) => res.sendFile(path.join(ROOT, 'export.js')));
// Bundle esbuild del frontend (cartella dist/): solo .js + .map, niente traversal.
app.get('/dist/:file', (req, res) => {
  if (!/^[a-zA-Z0-9_.-]+\.(js|map)$/.test(req.params.file)) return res.status(404).end();
  res.sendFile(path.join(ROOT, 'dist', req.params.file));
});
// Moduli condivisi browser/test (cartella lib/): solo file .js, niente traversal.
app.get('/lib/:file', (req, res) => {
  if (!/^[a-zA-Z0-9_-]+\.js$/.test(req.params.file)) return res.status(404).end();
  res.sendFile(path.join(ROOT, 'lib', req.params.file));
});

// Font Awesome 6 servito localmente da vendor/fontawesome (self-hosted, no CDN, no dep npm).
// Esposto solo: css/all.min.css + webfonts fa-*.woff2. Nessun traversal.
const FA_ROOT = path.join(ROOT, 'vendor', 'fontawesome');
app.get('/vendor/fontawesome/css/all.min.css', (_, res) => res.sendFile(path.join(FA_ROOT, 'css', 'all.min.css')));
app.get('/vendor/fontawesome/webfonts/:file', (req, res) => {
  if (!/^fa-[a-z0-9]+-\d+\.woff2$/i.test(req.params.file)) return res.status(404).end();
  res.sendFile(path.join(FA_ROOT, 'webfonts', req.params.file));
});

// ---- REST API v1 (server/routes/api-v1.js) ---------------------------------
// Read-only, gate a TOKEN Bearer. Montata PRIMA dell'auth di sessione: la
// superficie esterna NON passa dal requireAuth globale, si autentica solo col
// token e restituisce esclusivamente dati sanitizzati (lib/api-shape.js).
app.use(require('./server/routes/api-v1'));

// ---- Auth (sessioni + login page + route /api/auth/*) ----------------------
// Deve essere registrato PRIMA delle route protette.
auth.register(app);

// ---- Gestione token API (server/routes/api-tokens.js) ----------------------
// Solo admin (sessione). Mintare/revocare i token usati dall'API v1 qui sopra.
app.use(require('./server/routes/api-tokens'));

// ---- Frontend protetto ------------------------------------------------------

app.get('/', (_, res) => res.sendFile(path.join(ROOT, 'netmapper.html')));

// ---- Progetti (server/routes/projects.js) -----------------------------------
app.use(require('./server/routes/projects'));

// ---- Organizzazione multi-sede (server/routes/organization.js) --------------
// Il livello SOPRA i progetti: sedi, uplink WAN e collegamenti inter-sede.
// UNA per installazione (data/organization.json).
app.use(require('./server/routes/organization'));

// ---- Storico progetto (server/routes/history.js) ----------------------------
app.use(require('./server/routes/history'));

// ---- Skin store (server/routes/skins.js) ------------------------------------
app.use(require('./server/routes/skins'));

// ---- Catalogo device-type (server/routes/device-types.js) -------------------
app.use(require('./server/routes/device-types'));

// ---- Assistente AI (server/routes/ai.js) ------------------------------------
// Config (GET mascherato / PUT admin) + preview del contesto sanitizzato.
// Session-gated (montata dopo auth.register). La chiave resta server-side.
app.use(require('./server/routes/ai'));

// ---- Sincronizzazione DCIM/IPAM (server/routes/integrations.js) -------------
// Config (GET mascherato / PUT admin) + prova connessione + capabilities.
// Session-gated (dopo auth.register). Il token DCIM resta server-side; l'IMPORT
// arriva in Fase B, l'EXPORT vive nel modulo a pagamento modules/dcim-export/.
app.use(require('./server/routes/integrations'));

// ---- Primitive di rete / discovery (server/netscan.js) ----------------------
const { _execFileAsync, _pingHost, _normMac, _parseArpTable, _readArpMap, _readLocalInterfaceMap, _vendorByMac, _extractTitle, _httpProbe, _tcpProbe, _deepScanHost, _parseNetbiosOutput, _netbiosProbe, _parseNetViewOutput, _smbSharesProbe, _deepIdentityScanHost } = require('./server/netscan');

// ---- Classificazione device / discovery meta (server/classify.js) ----------
const { _cleanHostname, _penFromObjectId, _vendorByObjectId, _decodeSysServices, _resolveSysObject, _resolveOsFingerprint, _classifyDiscoveredDevice, _buildDiscoveryMeta, _decorateDiscoveryRow } = require('./server/classify');

// ---- Discovery: poll/discover/topology/crawl (server/routes/discovery.js) ---
app.use(require('./server/routes/discovery'));

// ---- Export PDF (server/routes/export.js) -----------------------------------
app.use(require('./server/routes/export'));

// ---- Moduli a pagamento (modules/<name>/server) — plugin generico -----------
// La cartella modules/ e' gitignored: presente solo nei deployment con moduli
// licenziati. Ogni modulo esporta function(app, ctx) e monta le sue route
// (auth.requireAdmin dove serve); via ctx dichiara una voce di menu e registra
// la pulizia dei propri sidecar alla cancellazione di un progetto. Il core resta
// ignaro del contenuto (contratto feature-agnostico, non legato ad alcun modulo).
const moduleRegistry = require('./server/module-registry');
app.get('/api/modules', (_, res) => res.json(moduleRegistry.getNav()));
moduleRegistry.loadModules(app, {
  auth,                                  // sessione + requireAdmin (in-process, nativi)
  projectsDir: PROJECTS_DIR,             // dove vivono i dati progetto: i moduli ci mettono i propri sidecar
  registerNav: moduleRegistry.registerNav,
  onProjectDelete: moduleRegistry.registerProjectDeleteHook,
}, path.join(ROOT, 'modules'));

// ---- 404 catch-all ----------------------------------------------------------

app.use((req, res) => res.status(404).json({ error: 'Not found' }));

// ---- Error handler (JSON, niente stack in chiaro) ---------------------------
// Cattura gli errori del body-parser (JSON malformato -> 400, payload >20mb ->
// 413) e QUALUNQUE throw sincrono nelle route. Senza questo middleware a 4
// argomenti Express usava il proprio finalhandler di default: pagina HTML con lo
// STACK e i path assoluti del server (info-disclosure) invece del JSON atteso
// dai consumer dell'API. La firma DEVE avere 4 parametri per essere riconosciuta
// come error-handler; `next` viene usato nel ramo headersSent.
//
// ⚠️ Sotto il 500 si rimandava `err.message` COM'E'. Per un errore del filesystem
// quel messaggio porta il PATH ASSOLUTO del server: una GET su /styles/<nome mai
// esistito>.css passava la regex del nome, cadeva nel sendFile, e rispondeva
// 404 con "ENOENT: ... stat 'C:\Users\...\InfranetPro\styles\x.css'" — misurato su
// /styles, /dist e /lib. Il 404 catch-all piu' sopra non li vede: un errore
// scavalca i middleware normali e arriva solo qui. I codici fs non dicono niente
// di utile a chi chiama: si risponde con la forma pulita e il dettaglio resta nel log.
const FS_ERRNO = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'ENOTDIR', 'ELOOP', 'ENAMETOOLONG']);
app.use((err, req, res, next) => {
  const status = err.status || err.statusCode || 500;
  if (status >= 500) console.error(`  [ERR] ${req.method} ${req.path}: ${err.message}`);
  if (res.headersSent) return next(err);                      // risposta gia' iniziata -> delega a Express
  const daFs = FS_ERRNO.has(err && err.code);
  if (daFs && status < 500) console.warn(`  [404] ${req.method} ${req.path}: ${err.code}`);
  const msg = status >= 500 ? 'Internal server error'
            : daFs ? (status === 404 ? 'Not found' : 'Bad request')
            : (err.message || 'Bad request');
  res.status(status).json({ error: msg });
});

// ---- Start ------------------------------------------------------------------

// Avvia il listen SOLO se eseguito direttamente (node server.js).
// Quando il file viene richiesto da un test (require), NON apre la porta.
if (require.main === module) {
  // Retrofit dei permessi: i file scritti prima della regola «le credenziali sono 0600» hanno la
  // maschera di default e li leggerebbe ogni utente del sistema. Una volta per avvio, solo qui (non
  // al require: i test puntano le cartelle altrove), best-effort e idempotente.
  try {
    const n = hardenTree(PROJECTS_DIR);
    for (const f of [auth.USERS_FILE, require('./server/api-tokens').TOKENS_FILE]) { hardenFile(f); hardenBackup(f); }
    if (n) console.log(`  [SICUREZZA] permessi dei dati ristretti a 0600: ${n} file`);
  } catch (e) { console.warn('  [SICUREZZA] retrofit permessi non riuscito:', e.message); }

  const server = app.listen(PORT, HOST, () => {
    const shownHost = (HOST === '0.0.0.0' || HOST === '::') ? 'localhost' : HOST;
    console.log('');
    console.log('  ================================================');
    console.log(`    InfraNet Pro  ->  http://${shownHost}:${PORT}`);
    console.log(`    Progetti in:   ${PROJECTS_DIR}`);
    console.log('    Premi Ctrl+C per fermare il server');
    console.log('  ================================================');
    console.log('');
  });

  server.on('error', err => {
    if (err.code === 'EADDRINUSE') {
      console.error(`\n  [ERRORE] La porta ${PORT} è già occupata.`);
      console.error('  Chiudi l\'altra istanza del server (finestra PowerShell o Node.js) e riprova.\n');
    } else {
      console.error(`\n  [ERRORE] ${err.message}\n`);
    }
    process.exit(1);
  });
}

// ---- Gestori errori globali - evitano crash silenti -----------------------

process.on('uncaughtException', err => {
  console.error(`\n  [FATAL] Eccezione non catturata: ${err.message}`);
  console.error(err.stack);
  // App locale: logga l'errore ma non chiudere il server durante workflow lunghi
  // come discovery/SNMP. Le route principali rispondono gia con errori JSON.
});

process.on('unhandledRejection', reason => {
  console.error(`\n  [WARNING] Promise rifiutata non gestita: ${reason}`);
});

// Funzioni pure di discovery esposte SOLO per i test di regressione.
// Additivo: nessun effetto sul comportamento runtime.
module.exports = {
  app,
  _internals: {
    _parseNetbiosOutput, _parseNetViewOutput, _cleanHostname,
    _penFromObjectId, _vendorByObjectId, _decodeSysServices,
    _resolveSysObject, _resolveOsFingerprint,
    _classifyDiscoveredDevice, _buildDiscoveryMeta,
  },
};