import * as express from 'express'; import * as path from 'path'; import * as fs from 'fs'; import * as morgan from 'morgan'; import { v4 as uuid } from 'uuid'; import signaling from './signaling'; import { log, LogLevel } from './log'; import Options from './class/options'; import { reset as resetHandler } from './class/httphandler'; import { initSwagger } from './swagger'; const cors = require('cors'); const multer = require('multer'); const AVATAR_UPLOAD_LIMIT_BYTES = 2 * 1024 * 1024; const ALLOWED_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']); const ALLOWED_AVATAR_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']); function safeAvatarExtension(file: any): string { const originalExt = path.extname(file.originalname || '').toLowerCase(); if (ALLOWED_AVATAR_EXTENSIONS.has(originalExt)) { return originalExt; } switch (file.mimetype) { case 'image/jpeg': return '.jpg'; case 'image/png': return '.png'; case 'image/webp': return '.webp'; case 'image/gif': return '.gif'; default: return ''; } } function isAllowedAvatar(file: any): boolean { return ALLOWED_AVATAR_MIME_TYPES.has(file.mimetype) && safeAvatarExtension(file).length > 0; } export const createServer = (config: Options): express.Express => { const app: express.Express = express(); resetHandler(config.mode); if (config.logging !== 'none') { app.use(morgan(config.logging)); } app.use(cors({ origin: '*' })); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.get('/config', (_req, res) => res.json({ useWebSocket: config.type === 'websocket', startupMode: config.mode, logging: config.logging, protocol: config.secure ? 'https' : 'http', port: config.port })); app.use('/signaling', signaling); app.use(express.static(path.join(__dirname, '../client/public'))); app.use('/module', express.static(path.join(__dirname, '../client/src'))); app.get('/', (_req, res) => { const indexPagePath = path.join(__dirname, '../client/public/index.html'); fs.access(indexPagePath, (err) => { if (err) { log(LogLevel.warn, `Can't find file '${indexPagePath}'`); res.status(404).send(`Can't find file ${indexPagePath}`); return; } res.sendFile(indexPagePath); }); }); initSwagger(app, config); const storage = multer.diskStorage({ destination: (_req: any, _file: any, cb: (error: Error | null, destination: string) => void) => { const uploadDir = path.join(__dirname, '../client/public/uploads/avatars'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: (_req: any, file: any, cb: (error: Error | null, filename: string) => void) => { cb(null, file.originalname); } }); const upload = multer({ storage, limits: { fileSize: AVATAR_UPLOAD_LIMIT_BYTES }, fileFilter: (_req: express.Request, file: any, cb: (error: Error | null, acceptFile?: boolean) => void) => { if (!isAllowedAvatar(file)) { cb(new Error('Only jpg, png, webp, or gif avatars are allowed')); return; } cb(null, true); } }); app.post('/api/upload/avatar', (req: express.Request, res: express.Response) => { upload.single('avatar')(req, res, (error: Error) => { if (error) { log(LogLevel.warn, 'Avatar upload rejected:', error.message); const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE'; res.status(400).json({ success: false, message: isSizeLimit ? 'Avatar file is too large' : error.message }); return; } const request = req as any; if (!request.file) { res.status(400).json({ success: false, message: 'No file uploaded' }); return; } const ext = safeAvatarExtension(request.file); if (!ext) { fs.unlink(request.file.path, () => undefined); res.status(400).json({ success: false, message: 'Unsupported avatar file type' }); return; } const oldPath = request.file.path; const newFilename = `avatar_${uuid()}${ext}`; const newPath = path.join(path.dirname(oldPath), newFilename); fs.rename(oldPath, newPath, (err) => { if (err) { log(LogLevel.error, 'Error renaming file:', err); res.status(500).json({ success: false, message: 'Avatar rename failed' }); return; } res.json({ success: true, avatarUrl: `/uploads/avatars/${newFilename}` }); }); }); }); app.use('/uploads', express.static(path.join(__dirname, '../client/public/uploads'))); return app; };