diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d2855f8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm audit:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f3f8aa9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,234 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CDN-APP-INSIDER is a self-hosted Content Delivery Network (CDN) application for secure file transfer and management. The application supports multiple authentication methods (Discord, LDAP/ActiveDirectory), file collaboration, and real-time WebSocket updates. + +**Version**: 1.2.0-beta +**Author**: Dinawo - Group Myaxrin Labs +**Main Contributor**: WaYy + +## Development Commands + +### Running the Application +```bash +npm start # Production mode +npm run nodemon # Development mode with auto-reload +``` + +### Installation +The application is typically installed via: +```bash +curl -s https://apollon.dinawo.fr/getcdn/install/latest | bash +``` + +After installation, access the dashboard at: `https://your-ip:3000/dpanel/dashboard` + +**Prerequisites**: A CDN-Access group must exist in your LDAP directory. + +## Architecture Overview + +### Core Components + +**server.js** - Application entry point that: +- Initializes Express app with session management and Passport authentication +- Configures authentication strategies based on `data/setup.json` (Discord, LDAP) +- Sets up WebSocket server for real-time updates +- Starts cron jobs for file cleanup and system reporting +- Protects sensitive JSON files in `/data/` directory +- Listens on port 3000 (configurable via `PORT` env variable) + +**routes/routes.js** - Central routing hub that imports and mounts all route modules with middleware chains + +### Authentication System + +The application uses Passport.js with multiple strategies: +- **Discord OAuth** (`models/Passport-Discord.js`) +- **LDAP/ActiveDirectory** (`models/Passport-ActiveDirectory.js`) +- **Google OAuth** (`models/Passport-Google.js`) + +Authentication strategies are conditionally loaded based on `data/setup.json` configuration. + +**authMiddleware.js** - Core authentication middleware that: +- Validates session authentication via `req.isAuthenticated()` +- Loads user data from `data/user.json` +- Attaches user object to `req.session.user`, `res.locals.user`, and `req.userData` +- Redirects unauthenticated users to `/auth/login` + +### Logging System (`config/logs.js`) + +Winston-based logging with: +- Daily rotating file logs (14-day retention, 20MB max size) +- Multiple specialized loggers: server, client, error, auth, suspicious, API, filesystem, database +- Configurable via `data/setup.json` with: + - `logs.enabled`: 'on' or 'off' + - `logs.level`: 'info', 'warn', 'error', etc. + - `logs.includeOnly`: Array of paths to exclusively log + - `logs.excludePaths`: Array of paths to exclude from logging + - `logs.levels`: Array of enabled log levels + +Logs are stored in `/logs/` directory with format: `log-YYYY-MM-DD.log` + +### Security & Ban System (`models/banModel.js`) + +Progressive ban system that: +- Tracks suspicious requests per IP in `data/banUser.json` +- Implements escalating ban levels: 10min, 30min, 60min, permanent +- Triggers after 5 suspicious requests within 60 seconds +- Excludes localhost and specific endpoints (ActiveDirectory, favicon) +- All suspicious activity is logged via `suspiciousLogger` + +**discordWebhookSuspisiousAlertMiddleware.js** - Sends Discord webhook alerts for suspicious API requests + +### WebSocket System (`models/websocketManager.js`) + +Real-time communication for: +- File collaboration status (who's viewing/editing) +- Broadcasting file updates to all connected clients +- Connection management keyed by userId and fileId +- Sends `fileStatus` messages with active users array + +Access via: `req.app.get('wsManager')` in routes + +### File Management + +**File Storage**: All uploaded files are stored in `/cdn-files/` directory + +**File Metadata**: Tracked in `data/file_info.json` with: +- File path, name, size, upload date +- Expiry date for automatic cleanup +- Owner information and permissions + +**File Cleanup Service** (`services/fileCleanupService.js`): +- Extends BaseService class +- Runs on cron schedule (default: hourly at `0 * * * *`) +- Removes expired files based on `expiryDate` +- Removes orphaned entries for missing files +- Updates `data/file_info.json` after cleanup + +**Report Service** (`services/reportService.js`): +- Generates system reports in `/report/` directory +- Runs on configurable cron schedule + +### Data Files + +Located in `/data/` directory (protected from direct HTTP access): +- **user.json** - User accounts and roles +- **setup.json** - Application configuration (auth providers, logging, etc.) +- **file_info.json** - File metadata registry +- **banUser.json** - IP ban tracking +- **collaboration.json** - File collaboration settings + +## Route Structure + +### Public Routes +- `/` - Landing page +- `/auth/login` - Login page +- `/auth/logout` - Logout handler +- `/auth/activedirectory` - AD/LDAP authentication callback +- `/auth/discord` - Discord OAuth callback +- `/attachments` - File serving endpoint +- `/build-metadata` - Build information + +### Dashboard Routes (`/dpanel/dashboard`) +- `/dpanel/dashboard` - Main dashboard (requires auth) +- `/dpanel/dashboard/folder` - Folder view +- `/dpanel/dashboard/profil` - User profile +- `/dpanel/upload` - File upload interface + +### Admin Routes (`/dpanel/dashboard/admin`) +Require admin role: +- `/dpanel/dashboard/admin` - Admin panel +- `/dpanel/dashboard/admin/users` - User management +- `/dpanel/dashboard/admin/settingsetup` - System settings +- `/dpanel/dashboard/admin/stats-logs` - Statistics and logs +- `/dpanel/dashboard/admin/Privacy-Security` - Security settings + +### API Routes (`/api/dpanel`) +All API routes use: +1. `discordWebhookSuspisiousAlertMiddleware` - Alerts on suspicious activity +2. `logApiRequest` - Logs API calls with timing + +Key endpoints: +- POST `/api/dpanel/upload` - File upload +- POST `/api/dpanel/dashboard/newfolder` - Create folder +- PUT `/api/dpanel/dashboard/rename` - Rename file +- PUT `/api/dpanel/folders/rename` - Rename folder +- DELETE `/api/dpanel/dashboard/delete` - Delete file +- DELETE `/api/dpanel/dashboard/deletefolder` - Delete folder +- POST `/api/dpanel/dashboard/movefile` - Move file +- POST `/api/dpanel/collaboration` - Manage file collaboration +- GET `/api/dpanel/users/search` - Search users +- GET/POST `/api/dpanel/sharedfolders` - Shared folder operations +- POST `/api/dpanel/generate-token` - Generate API token +- POST `/api/dpanel/revoke-token` - Revoke API token + +### API Documentation +Swagger UI available at: `/api/docs` + +## Middleware Chain + +Standard middleware chain for protected routes: +``` +authMiddleware → discordWebhookSuspisiousAlertMiddleware → logApiRequest → route handler +``` + +## Frontend + +- **View Engine**: EJS templates in `/views/` +- **Static Assets**: `/public/` directory +- **CSS**: Custom dashboard styles in `/public/css/dashboard.styles.css` +- **JavaScript**: Client-side logic in `/public/js/dashboard.js` +- **Styling**: TailwindCSS + DaisyUI components + +## Key Dependencies + +- **express** - Web framework +- **passport** - Authentication +- **socket.io** & **ws** - WebSocket support +- **winston** - Logging +- **node-cron** - Scheduled tasks +- **multer** & **express-fileupload** - File uploads +- **pg** & **mysql2** - Database support +- **bcrypt** - Password hashing +- **jsonwebtoken** - JWT tokens + +## Important Notes + +### Security Considerations +- All `/data/*.json` files are protected from direct HTTP access +- Session secrets are generated using `crypto.randomBytes(64)` +- Cookies are secure in production (`NODE_ENV=production`) +- Rate limiting via `express-rate-limit` +- Progressive ban system for suspicious activity + +### File Paths +- Always use `path.join(__dirname, ...)` for file paths +- Normalize paths with `path.normalize()` and replace backslashes +- File paths in metadata use forward slashes + +### Session Management +- User data is stored in both session and attached to `req.userData` +- Session maxAge: 24 hours +- Sessions persist across server restarts via session storage + +### Error Handling +- Global error handlers catch uncaught exceptions and unhandled rejections +- Errors are logged via `ErrorLogger` from config/logs +- API errors return JSON with `{ error, message }` structure +- HTML requests receive rendered error pages + +### WebSocket Events +- `join` - User joins file view (params: userId, fileId) +- `leave` - User leaves file view (params: fileId) +- `fileStatus` - Broadcast of active users on a file + +## Testing & Debugging + +- Winston logs are colorized in console for easier debugging +- Request logging includes IP, User-Agent, method, URL, and timing +- API requests log response status and duration +- Suspicious activity is highlighted with orange prefix +- Error logs include full stack traces diff --git a/Middlewares/csrfProtectionMiddleware.js b/Middlewares/csrfProtectionMiddleware.js new file mode 100644 index 0000000..1b2a540 --- /dev/null +++ b/Middlewares/csrfProtectionMiddleware.js @@ -0,0 +1,170 @@ +/** + * Middleware de protection CSRF (Cross-Site Request Forgery) + * Génère et valide des tokens CSRF pour les requêtes sensibles + */ + +const crypto = require('crypto'); +const { logger } = require('../config/logs'); + +// Stockage en mémoire des tokens CSRF (en production, utiliser Redis ou une base de données) +const csrfTokens = new Map(); + +// Durée de vie d'un token CSRF (30 minutes) +const TOKEN_LIFETIME = 30 * 60 * 1000; + +/** + * Génère un token CSRF pour une session + */ +const generateCsrfToken = (sessionId) => { + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = Date.now() + TOKEN_LIFETIME; + + csrfTokens.set(sessionId, { + token, + expiresAt + }); + + // Nettoyer les tokens expirés toutes les 5 minutes + setInterval(() => { + const now = Date.now(); + for (const [id, data] of csrfTokens.entries()) { + if (data.expiresAt < now) { + csrfTokens.delete(id); + } + } + }, 5 * 60 * 1000); + + return token; +}; + +/** + * Valide un token CSRF + */ +const validateCsrfToken = (sessionId, token) => { + const storedData = csrfTokens.get(sessionId); + + if (!storedData) { + return false; + } + + if (storedData.expiresAt < Date.now()) { + csrfTokens.delete(sessionId); + return false; + } + + return storedData.token === token; +}; + +/** + * Middleware pour ajouter le token CSRF à la requête + */ +const csrfTokenMiddleware = (req, res, next) => { + // Générer un ID de session si nécessaire + if (!req.session) { + return next(); + } + + const sessionId = req.session.id || req.sessionID; + + // Générer ou récupérer le token CSRF + let csrfToken = null; + const storedData = csrfTokens.get(sessionId); + + if (storedData && storedData.expiresAt > Date.now()) { + csrfToken = storedData.token; + } else { + csrfToken = generateCsrfToken(sessionId); + } + + // Ajouter le token aux locals pour l'utiliser dans les vues + res.locals.csrfToken = csrfToken; + + // Ajouter une méthode helper + req.csrfToken = () => csrfToken; + + next(); +}; + +/** + * Middleware de validation CSRF pour les méthodes POST, PUT, DELETE + */ +const csrfProtectionMiddleware = (req, res, next) => { + // Ignorer les requêtes GET, HEAD, OPTIONS + if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { + return next(); + } + + // Ignorer certaines routes (API publiques, webhooks, etc.) + const exemptPaths = [ + '/auth/activedirectory/callback', + '/auth/discord/callback', + '/api/webhook' + ]; + + if (exemptPaths.some(path => req.path.startsWith(path))) { + return next(); + } + + // Vérifier si la session existe + if (!req.session) { + logger.warn(`CSRF protection: No session found for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Session invalide', + code: 'NO_SESSION' + }); + } + + const sessionId = req.session.id || req.sessionID; + + // Récupérer le token CSRF de la requête + const token = req.body._csrf || + req.query._csrf || + req.headers['x-csrf-token'] || + req.headers['csrf-token']; + + if (!token) { + logger.warn(`CSRF protection: No token provided for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Token CSRF manquant', + code: 'CSRF_TOKEN_MISSING' + }); + } + + // Valider le token + if (!validateCsrfToken(sessionId, token)) { + logger.warn(`CSRF protection: Invalid token for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Token CSRF invalide ou expiré', + code: 'CSRF_TOKEN_INVALID' + }); + } + + next(); +}; + +/** + * Route pour obtenir un nouveau token CSRF + */ +const getCsrfToken = (req, res) => { + const sessionId = req.session?.id || req.sessionID; + + if (!sessionId) { + return res.status(400).json({ + error: 'Session invalide' + }); + } + + const token = generateCsrfToken(sessionId); + + res.json({ + csrfToken: token + }); +}; + +module.exports = { + csrfTokenMiddleware, + csrfProtectionMiddleware, + getCsrfToken, + generateCsrfToken, + validateCsrfToken +}; diff --git a/Middlewares/inputValidationMiddleware.js b/Middlewares/inputValidationMiddleware.js new file mode 100644 index 0000000..5571996 --- /dev/null +++ b/Middlewares/inputValidationMiddleware.js @@ -0,0 +1,202 @@ +/** + * Middleware de validation et d'assainissement des entrées utilisateur + * Protège contre les injections SQL, XSS, et autres attaques + */ + +const { logger } = require('../config/logs'); + +/** + * Nettoie une chaîne de caractères pour prévenir les injections + */ +const sanitizeString = (str) => { + if (typeof str !== 'string') return str; + + // Supprime les caractères null bytes + str = str.replace(/\0/g, ''); + + // Encode les caractères spéciaux HTML + str = str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + + return str.trim(); +}; + +/** + * Valide un nom de fichier/dossier + */ +const validateFileName = (filename) => { + if (!filename || typeof filename !== 'string') { + return { valid: false, error: 'Nom de fichier invalide' }; + } + + // Limite de longueur + if (filename.length > 255) { + return { valid: false, error: 'Nom de fichier trop long (max 255 caractères)' }; + } + + // Caractères interdits dans les noms de fichiers Windows/Linux + const forbiddenChars = /[<>:"\/\\|?*\x00-\x1f]/g; + if (forbiddenChars.test(filename)) { + return { valid: false, error: 'Caractères interdits dans le nom de fichier' }; + } + + // Noms réservés Windows + const reservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i; + if (reservedNames.test(filename.split('.')[0])) { + return { valid: false, error: 'Nom de fichier réservé' }; + } + + // Vérifier les tentatives de traversée de répertoire + if (filename.includes('..') || filename.includes('./') || filename.includes('.\\')) { + return { valid: false, error: 'Tentative de traversée de répertoire détectée' }; + } + + return { valid: true }; +}; + +/** + * Valide un chemin de dossier + */ +const validatePath = (path) => { + if (!path || typeof path !== 'string') { + return { valid: false, error: 'Chemin invalide' }; + } + + // Vérifier les tentatives de traversée de répertoire + if (path.includes('..') || path.includes('~')) { + return { valid: false, error: 'Tentative de traversée de répertoire détectée' }; + } + + // Vérifier les chemins absolus non autorisés + if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) { + return { valid: false, error: 'Chemin absolu non autorisé' }; + } + + return { valid: true }; +}; + +/** + * Valide un ID utilisateur + */ +const validateUserId = (userId) => { + if (!userId) { + return { valid: false, error: 'ID utilisateur manquant' }; + } + + // Accepte les formats Discord (snowflake) ou alphanumériques + if (!/^[a-zA-Z0-9_-]{1,100}$/.test(userId)) { + return { valid: false, error: 'Format d\'ID utilisateur invalide' }; + } + + return { valid: true }; +}; + +/** + * Valide une adresse email + */ +const validateEmail = (email) => { + if (!email || typeof email !== 'string') { + return { valid: false, error: 'Email invalide' }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { valid: false, error: 'Format d\'email invalide' }; + } + + if (email.length > 254) { + return { valid: false, error: 'Email trop long' }; + } + + return { valid: true }; +}; + +/** + * Nettoie récursivement un objet + */ +const sanitizeObject = (obj) => { + if (obj === null || obj === undefined) return obj; + + if (typeof obj === 'string') { + return sanitizeString(obj); + } + + if (Array.isArray(obj)) { + return obj.map(item => sanitizeObject(item)); + } + + if (typeof obj === 'object') { + const sanitized = {}; + for (const [key, value] of Object.entries(obj)) { + sanitized[key] = sanitizeObject(value); + } + return sanitized; + } + + return obj; +}; + +/** + * Middleware principal de validation + */ +const inputValidationMiddleware = (req, res, next) => { + try { + // Log des requêtes suspectes + const suspiciousPatterns = [ + / -
- +