This commit is contained in:
2026-05-23 22:47:34 +08:00
parent 690ebac266
commit 5fdc70c645
3 changed files with 138 additions and 15 deletions

View File

@@ -581,6 +581,10 @@ function onMessage(ws: WebSocket, message: any): void {
const connectionId = message.connectionId; const connectionId = message.connectionId;
const chatMessage = message.message; const chatMessage = message.message;
const senderParticipantId = (ws as any).participantId; 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) { if (chatMessage && chatMessage.type === 'user-info' && chatMessage.data) {
(ws as any).userInfo = { (ws as any).userInfo = {
id: chatMessage.data.id || '', id: chatMessage.data.id || '',

View File

@@ -2,6 +2,7 @@ import * as express from 'express';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as morgan from 'morgan'; import * as morgan from 'morgan';
import { v4 as uuid } from 'uuid';
import signaling from './signaling'; import signaling from './signaling';
import { log, LogLevel } from './log'; import { log, LogLevel } from './log';
import Options from './class/options'; import Options from './class/options';
@@ -11,6 +12,34 @@ import { initSwagger } from './swagger';
const cors = require('cors'); const cors = require('cors');
const multer = require('multer'); 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 => { export const createServer = (config: Options): express.Express => {
const app: express.Express = express(); const app: express.Express = express();
resetHandler(config.mode); resetHandler(config.mode);
@@ -62,18 +91,45 @@ 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 // 头像上传API
app.post('/api/upload/avatar', upload.single('avatar'), (req: any, res: express.Response) => { app.post('/api/upload/avatar', (req: express.Request, res: express.Response) => {
if (!req.file) { 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 request = req as any;
if (!request.file) {
return res.status(400).json({ success: false, message: 'No file uploaded' }); return res.status(400).json({ success: false, message: 'No file uploaded' });
} }
const userId = req.body.userId || 'unknown'; const ext = safeAvatarExtension(request.file);
const ext = path.extname(req.file.originalname); if (!ext) {
const oldPath = req.file.path; fs.unlink(request.file.path, () => undefined);
const newFilename = `${userId}${ext}`; 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); const newPath = path.join(path.dirname(oldPath), newFilename);
// 重命名文件 // 重命名文件
@@ -87,6 +143,7 @@ export const createServer = (config: Options): express.Express => {
res.json({ success: true, avatarUrl: avatarUrl }); res.json({ success: true, avatarUrl: avatarUrl });
}); });
}); });
});
// 确保uploads目录可访问 // 确保uploads目录可访问
app.use('/uploads', express.static(path.join(__dirname, '../client/public/uploads'))); app.use('/uploads', express.static(path.join(__dirname, '../client/public/uploads')));

View File

@@ -4,6 +4,58 @@ import { Server as HttpsServer } from 'https';
import * as handler from "./class/websockethandler"; import * as handler from "./class/websockethandler";
import { log, LogLevel } from './log'; 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 { export default class WSSignaling {
server: HttpServer | HttpsServer; server: HttpServer | HttpsServer;
wss: websocket.Server; 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) { if (!msg || !this) {
return; return;
@@ -76,39 +128,48 @@ export default class WSSignaling {
// 根据消息类型处理 // 根据消息类型处理
switch (msg.type) { switch (msg.type) {
case "connect": case "connect":
if (!hasConnectionId(msg)) return;
handler.onConnect(ws, msg.connectionId); handler.onConnect(ws, msg.connectionId);
break; break;
case "disconnect": case "disconnect":
if (!hasConnectionId(msg)) return;
handler.onDisconnect(ws, msg.connectionId); handler.onDisconnect(ws, msg.connectionId);
break; break;
case "offer": case "offer":
if (!hasData(msg)) return;
if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onOffer(ws, msg.data); handler.onOffer(ws, msg.data);
break; break;
case "answer": case "answer":
if (!hasData(msg)) return;
if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onAnswer(ws, msg.data); handler.onAnswer(ws, msg.data);
break; break;
case "candidate": case "candidate":
if (!hasData(msg)) return;
if (msg.participantId !== undefined) msg.data.participantId = msg.participantId; if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onCandidate(ws, msg.data); handler.onCandidate(ws, msg.data);
break; break;
case "ping": case "ping":
ws.send(JSON.stringify({ type: "pong" })); sendJson(ws, { type: "pong" });
break; break;
case "pong": case "pong":
(ws as any).lastActivity = Date.now(); (ws as any).lastActivity = Date.now();
break; break;
case "broadcast": case "broadcast":
if (!hasData(msg)) return;
handler.onBroadcast(ws, msg.data); handler.onBroadcast(ws, msg.data);
break; break;
case 'call-request': case 'call-request':
if (!hasData(msg)) return;
handler.onCallConnectionId(ws, msg.data); handler.onCallConnectionId(ws, msg.data);
break; break;
case 'host-userInfo': case 'host-userInfo':
if (!hasData(msg)) return;
handler.onHostUserInfo(ws, msg.data); handler.onHostUserInfo(ws, msg.data);
break; break;
case 'invite-call': case 'invite-call':
if (!hasData(msg)) return;
handler.onInviteCall(ws, msg.data); handler.onInviteCall(ws, msg.data);
break; break;
// case 'invite-accepted': // case 'invite-accepted':
@@ -118,6 +179,7 @@ export default class WSSignaling {
// handler.onInviteRejected(ws, msg.data); // handler.onInviteRejected(ws, msg.data);
// break; // break;
case 'on-message': case 'on-message':
if (!hasData(msg)) return;
if (msg.from) msg.data.connectionId = msg.from; if (msg.from) msg.data.connectionId = msg.from;
handler.onMessage(ws, msg.data); handler.onMessage(ws, msg.data);
break; break;