diff --git a/src/class/websockethandler.ts b/src/class/websockethandler.ts index 7d5b074..f4d307a 100644 --- a/src/class/websockethandler.ts +++ b/src/class/websockethandler.ts @@ -581,6 +581,10 @@ function onMessage(ws: WebSocket, message: any): void { const connectionId = message.connectionId; const chatMessage = message.message; const senderParticipantId = (ws as any).participantId; + if (!connectionId || !chatMessage || typeof chatMessage !== 'object') { + log(LogLevel.warn, 'Ignored malformed on-message payload:', message); + return; + } if (chatMessage && chatMessage.type === 'user-info' && chatMessage.data) { (ws as any).userInfo = { id: chatMessage.data.id || '', diff --git a/src/server.ts b/src/server.ts index 861273e..b7ec123 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ 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'; @@ -11,6 +12,34 @@ 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 { + const ext = path.extname(file.originalname || '').toLowerCase(); + return ALLOWED_AVATAR_MIME_TYPES.has(file.mimetype) && ALLOWED_AVATAR_EXTENSIONS.has(ext); +} + export const createServer = (config: Options): express.Express => { const app: express.Express = express(); resetHandler(config.mode); @@ -62,30 +91,58 @@ export const createServer = (config: Options): express.Express => { } }); - const upload = multer({ storage: storage }); + const upload = multer({ + storage: 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); + } + }); // 头像上传API - app.post('/api/upload/avatar', upload.single('avatar'), (req: any, res: express.Response) => { - if (!req.file) { - return res.status(400).json({ success: false, message: 'No file uploaded' }); - } + 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'; + return res.status(400).json({ + success: false, + message: isSizeLimit ? 'Avatar file is too large' : error.message + }); + } - const userId = req.body.userId || 'unknown'; - const ext = path.extname(req.file.originalname); - const oldPath = req.file.path; - const newFilename = `${userId}${ext}`; - const newPath = path.join(path.dirname(oldPath), newFilename); + const request = req as any; + if (!request.file) { + return res.status(400).json({ success: false, message: 'No file uploaded' }); + } + + const ext = safeAvatarExtension(request.file); + if (!ext) { + fs.unlink(request.file.path, () => undefined); + return res.status(400).json({ success: false, message: 'Unsupported avatar file type' }); + } + + 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); + fs.rename(oldPath, newPath, (err) => { + if (err) { + log(LogLevel.error, 'Error renaming file:', err); return res.status(500).json({ success: false, message: '文件重命名失败' }); } const avatarUrl = `/uploads/avatars/${newFilename}`; res.json({ success: true, avatarUrl: avatarUrl }); }); + }); }); // 确保uploads目录可访问 diff --git a/src/websocket.ts b/src/websocket.ts index 5b78278..6137ca3 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -4,6 +4,58 @@ import { Server as HttpsServer } from 'https'; import * as handler from "./class/websockethandler"; import { log, LogLevel } from './log'; +const VALID_MESSAGE_TYPES = new Set([ + "connect", + "disconnect", + "offer", + "answer", + "candidate", + "ping", + "pong", + "broadcast", + "call-request", + "host-userInfo", + "invite-call", + "on-message", +]); + +function sendJson(ws: WebSocket, payload: unknown): void { + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify(payload)); + } +} + +function parseWsMessage(raw: unknown): any | null { + if (typeof raw !== 'string') { + log(LogLevel.warn, 'WS ignored non-string message'); + return null; + } + + try { + const msg = JSON.parse(raw); + if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') { + log(LogLevel.warn, 'WS ignored malformed message:', raw); + return null; + } + if (!VALID_MESSAGE_TYPES.has(msg.type)) { + log(LogLevel.warn, `WS ignored unsupported message type: ${msg.type}`); + return null; + } + return msg; + } catch (error) { + log(LogLevel.warn, 'WS ignored invalid JSON message:', error); + return null; + } +} + +function hasConnectionId(msg: any): boolean { + return typeof msg.connectionId === 'string' && msg.connectionId.length > 0; +} + +function hasData(msg: any): boolean { + return msg.data && typeof msg.data === 'object'; +} + export default class WSSignaling { server: HttpServer | HttpsServer; wss: websocket.Server; @@ -64,7 +116,7 @@ export default class WSSignaling { // } // 解析消息数据 - const msg = JSON.parse(event.data); + const msg = parseWsMessage(event.data); // 检查消息是否有效 if (!msg || !this) { return; @@ -76,39 +128,48 @@ export default class WSSignaling { // 根据消息类型处理 switch (msg.type) { case "connect": + if (!hasConnectionId(msg)) return; handler.onConnect(ws, msg.connectionId); break; case "disconnect": + if (!hasConnectionId(msg)) return; handler.onDisconnect(ws, msg.connectionId); break; case "offer": + if (!hasData(msg)) return; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; handler.onOffer(ws, msg.data); break; case "answer": + if (!hasData(msg)) return; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; handler.onAnswer(ws, msg.data); break; case "candidate": + if (!hasData(msg)) return; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; handler.onCandidate(ws, msg.data); break; case "ping": - ws.send(JSON.stringify({ type: "pong" })); + sendJson(ws, { type: "pong" }); break; case "pong": (ws as any).lastActivity = Date.now(); break; case "broadcast": + if (!hasData(msg)) return; handler.onBroadcast(ws, msg.data); break; case 'call-request': + if (!hasData(msg)) return; handler.onCallConnectionId(ws, msg.data); break; case 'host-userInfo': + if (!hasData(msg)) return; handler.onHostUserInfo(ws, msg.data); break; case 'invite-call': + if (!hasData(msg)) return; handler.onInviteCall(ws, msg.data); break; // case 'invite-accepted': @@ -118,6 +179,7 @@ export default class WSSignaling { // handler.onInviteRejected(ws, msg.data); // break; case 'on-message': + if (!hasData(msg)) return; if (msg.from) msg.data.connectionId = msg.from; handler.onMessage(ws, msg.data); break;