#!/usr/bin/env node // Serveur WebSocket simple pour recevoir les logs temps réel const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const pino = require('pino'); const http = require('http'); const url = require('url'); const crypto = require('crypto'); const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); const wsPort = 8082; const httpPort = 8083; // Configuration du logger Pino avec fichier daté const now = new Date(); const timestamp = now.toISOString().slice(0, 10) + '_' + now.toLocaleTimeString('fr-FR').replace(/:/g, '-'); const logFile = path.join(__dirname, '..', 'logs', `class-generator-${timestamp}.log`); // Créer le dossier logs s'il n'existe pas const logsDir = path.join(__dirname, '..', 'logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); console.log('📁 Dossier logs créé'); } // Logger Pino avec sortie fichier et console const logger = pino( { level: 'debug', timestamp: pino.stdTimeFunctions.isoTime, }, pino.multistream([ { stream: pino.destination({ dest: logFile, mkdir: true, sync: false }) }, { stream: pino.transport({ target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss.l' } }) } ]) ); // Configuration DigitalOcean const DO_CONFIG = { ACCESS_KEY: 'DO8018LC8QF7CFBF7E2K', SECRET_KEY: 'RLH4bUidH4zb1XQAtBUeUnA4vjizdkQ78D1fOZ5gYpk', REGION: 'fra1', ENDPOINT: 'https://autocollant.fra1.digitaloceanspaces.com', BUCKET_ENDPOINT: 'https://fra1.digitaloceanspaces.com', BUCKET: 'autocollant', CONTENT_PATH: 'Class_generator/ContentMe' }; // Fonctions AWS Signature function sha256(message) { return crypto.createHash('sha256').update(message, 'utf8').digest('hex'); } function hmacSha256(key, message) { return crypto.createHmac('sha256', key).update(message, 'utf8').digest(); } async function generateAWSSignature(method, targetUrl) { const now = new Date(); const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '') + 'Z'; const urlObj = new URL(targetUrl); const host = urlObj.hostname; const canonicalUri = urlObj.pathname; const canonicalQueryString = urlObj.search ? urlObj.search.slice(1) : ''; const canonicalHeaders = `host:${host}\nx-amz-date:${timeStamp}\n`; const signedHeaders = 'host;x-amz-date'; const payloadHash = method === 'GET' ? sha256('') : 'UNSIGNED-PAYLOAD'; const canonicalRequest = [ method, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash ].join('\n'); const algorithm = 'AWS4-HMAC-SHA256'; const credentialScope = `${dateStamp}/${DO_CONFIG.REGION}/s3/aws4_request`; const canonicalRequestHash = sha256(canonicalRequest); const stringToSign = [ algorithm, timeStamp, credentialScope, canonicalRequestHash ].join('\n'); const kDate = hmacSha256('AWS4' + DO_CONFIG.SECRET_KEY, dateStamp); const kRegion = hmacSha256(kDate, DO_CONFIG.REGION); const kService = hmacSha256(kRegion, 's3'); const kSigning = hmacSha256(kService, 'aws4_request'); const signature = hmacSha256(kSigning, stringToSign).toString('hex'); const authorization = `${algorithm} Credential=${DO_CONFIG.ACCESS_KEY}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; return { 'Authorization': authorization, 'X-Amz-Date': timeStamp, 'X-Amz-Content-Sha256': payloadHash }; } // Serveur HTTP proxy pour DigitalOcean const httpServer = http.createServer(async (req, res) => { // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (req.method === 'GET' && req.url === '/do-proxy/_list') { // Route spéciale pour lister tous les fichiers dans le dossier logger.info(`📂 DÉBUT - Listing du dossier ContentMe`); try { // Utiliser l'endpoint du bucket directement const listUrl = `${DO_CONFIG.BUCKET_ENDPOINT}/${DO_CONFIG.BUCKET}?list-type=2&prefix=${DO_CONFIG.CONTENT_PATH}/`; logger.info(`🌐 List URL: ${listUrl}`); const headers = await generateAWSSignature('GET', listUrl); logger.info(`📋 Headers générés pour listing: ${JSON.stringify(headers, null, 2)}`); const response = await fetch(listUrl, { method: 'GET', headers: headers }); if (response.ok) { const xmlText = await response.text(); logger.info(`✅ Listing reçu: ${xmlText.length} caractères`); // Parser le XML pour extraire les noms de fichiers const fileList = parseS3ListResponse(xmlText); res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ files: fileList })); logger.info(`📁 ${fileList.length} fichiers listés`); } else { const errorText = await response.text(); logger.error(`❌ Erreur listing: ${response.status} - ${errorText}`); // Si c'est un 403, essayer une approche différente if (response.status === 403) { logger.info(`🔄 Tentative avec URL alternative...`); // Pour l'instant on retourne l'erreur res.writeHead(403, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ error: 'ListBucket permission denied', message: 'Les clés actuelles ne permettent pas le listing. Utilisation de la liste connue.', knownFiles: ['english-class-demo.json', 'sbs-level-7-8-new.json'] })); } else { res.writeHead(500, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' }); res.end('Error listing files'); } } } catch (error) { logger.error(`💥 Exception listing: ${error.message}`); res.writeHead(500, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' }); res.end(`Error: ${error.message}`); } return; } if ((req.method === 'GET' || req.method === 'HEAD') && req.url.startsWith('/do-proxy/')) { const filename = req.url.replace('/do-proxy/', ''); const targetUrl = `${DO_CONFIG.ENDPOINT}/${DO_CONFIG.CONTENT_PATH}/${filename}`; logger.info(`🔗 DÉBUT - Proxy ${req.method} request: ${filename}`); logger.info(`🌐 Target URL: ${targetUrl}`); logger.info(`🔑 ACCESS_KEY: ${DO_CONFIG.ACCESS_KEY}`); logger.info(`🔐 SECRET_KEY: ${DO_CONFIG.SECRET_KEY.substring(0, 8)}...`); try { logger.info(`🔧 Génération de la signature AWS...`); const headers = await generateAWSSignature(req.method, targetUrl); logger.info(`📋 Headers générés: ${JSON.stringify(headers, null, 2)}`); logger.info(`📤 Envoi de la requête à DigitalOcean...`); const fetchStart = Date.now(); const response = await fetch(targetUrl, { method: req.method, headers: headers }); const fetchDuration = Date.now() - fetchStart; logger.info(`⏱️ Durée requête DO: ${fetchDuration}ms`); logger.info(`📡 Status DO: ${response.status} ${response.statusText}`); logger.info(`📝 Headers réponse DO: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`); if (response.ok) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); if (req.method === 'GET') { const content = await response.text(); res.end(content); logger.info(`✅ SUCCÈS - Proxy GET: ${filename} (${content.length} chars)`); } else { // HEAD request - no body res.end(); logger.info(`✅ SUCCÈS - Proxy HEAD: ${filename} (headers only)`); } } else { const errorText = await response.text(); logger.error(`❌ ERREUR DO - Status: ${response.status}, Body: ${errorText}`); res.writeHead(response.status, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' }); res.end(errorText); } } catch (error) { logger.error(`💥 EXCEPTION PROXY - Type: ${error.constructor.name}`); logger.error(`💥 Message: ${error.message}`); logger.error(`💥 Stack: ${error.stack}`); res.writeHead(500, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' }); res.end(`Error: ${error.message}`); } return; } // Route par défaut res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found\nUse: /do-proxy/filename.json'); }); httpServer.listen(httpPort, '0.0.0.0', () => { logger.info(`🌐 Serveur HTTP proxy démarré sur le port ${httpPort} (toutes interfaces)`); logger.info(`🔗 URL proxy: http://localhost:${httpPort}/do-proxy/filename.json`); logger.info(`🌍 Accessible depuis Windows: http://localhost:${httpPort}/do-proxy/filename.json`); }); // Créer le serveur WebSocket sur toutes les interfaces const wss = new WebSocket.Server({ port: wsPort, host: '0.0.0.0' }); logger.info(`🚀 Serveur WebSocket démarré sur le port ${wsPort}`); logger.info(`📡 En attente de connexions...`); logger.info(`📝 Logs enregistrés dans: ${logFile}`); // Garder trace des clients connectés const clients = new Set(); wss.on('connection', function connection(ws) { logger.info('✅ Nouveau client connecté'); clients.add(ws); // Envoyer un message de bienvenue const welcomeMessage = { timestamp: new Date().toISOString(), level: 'INFO', message: '🎉 Connexion WebSocket établie - Logs en temps réel actifs' }; ws.send(JSON.stringify(welcomeMessage)); // Gérer les messages du client (si nécessaire) ws.on('message', function incoming(data) { try { const message = JSON.parse(data); // Enregistrer le log dans le fichier via Pino const level = (message.level || 'INFO').toLowerCase(); switch (level) { case 'error': logger.error(message.message); break; case 'warn': case 'warning': logger.warn(message.message); break; case 'debug': logger.debug(message.message); break; case 'trace': logger.trace(message.message); break; default: logger.info(message.message); } // DIFFUSER LE LOG À TOUS LES CLIENTS CONNECTÉS ! broadcastLog(message); } catch (error) { logger.warn('📨 Message reçu (brut):', data.toString()); } }); // Nettoyer quand le client se déconnecte ws.on('close', function close() { logger.info('❌ Client déconnecté'); clients.delete(ws); }); // Gérer les erreurs ws.on('error', function error(err) { logger.error('❌ Erreur WebSocket:', err.message); clients.delete(ws); }); }); // Fonction pour diffuser un log à tous les clients connectés function broadcastLog(logData) { const message = JSON.stringify(logData); let sentCount = 0; clients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { try { ws.send(message); sentCount++; } catch (error) { logger.error('❌ Erreur envoi vers client:', error.message); clients.delete(ws); } } else { // Nettoyer les connexions fermées clients.delete(ws); } }); if (sentCount > 0) { logger.debug(`📡 Log diffusé à ${sentCount} client(s): [${logData.level}] ${logData.message.substring(0, 50)}${logData.message.length > 50 ? '...' : ''}`); } } // Parser la réponse XML S3 pour extraire les noms de fichiers function parseS3ListResponse(xmlText) { const files = []; // Simple regex pour extraire les clés (noms de fichiers) du XML const keyRegex = /([^<]+)<\/Key>/g; let match; while ((match = keyRegex.exec(xmlText)) !== null) { const fullPath = match[1]; // Extraire juste le nom du fichier (après le dernier /) const filename = fullPath.split('/').pop(); // Filtrer les fichiers vides et les dossiers if (filename && filename.length > 0 && !filename.endsWith('/')) { files.push(filename); } } return files; } // Export pour utilisation dans d'autres modules if (typeof module !== 'undefined' && module.exports) { module.exports = { broadcastLog, wss, clients }; } // Gérer l'arrêt propre process.on('SIGINT', () => { logger.info('\n🛑 Arrêt du serveur WebSocket...'); // Fermer toutes les connexions clients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.close(); } }); // Fermer le serveur wss.close(() => { logger.info('✅ Serveur WebSocket arrêté'); logger.flush(); process.exit(0); }); }); // Message de test toutes les 30 secondes pour vérifier que ça marche setInterval(() => { if (clients.size > 0) { const testMessage = { timestamp: new Date().toISOString(), level: 'DEBUG', message: `💓 Heartbeat - ${clients.size} client(s) connecté(s)` }; broadcastLog(testMessage); } }, 30000);