优化
This commit is contained in:
@@ -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 || '',
|
||||||
|
|||||||
@@ -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,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
|
// 头像上传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) => {
|
||||||
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
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 request = req as any;
|
||||||
const ext = path.extname(req.file.originalname);
|
if (!request.file) {
|
||||||
const oldPath = req.file.path;
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
const newFilename = `${userId}${ext}`;
|
}
|
||||||
const newPath = path.join(path.dirname(oldPath), newFilename);
|
|
||||||
|
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) => {
|
fs.rename(oldPath, newPath, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log(LogLevel.error, 'Error renaming file:', err);
|
log(LogLevel.error, 'Error renaming file:', err);
|
||||||
return res.status(500).json({ success: false, message: '文件重命名失败' });
|
return res.status(500).json({ success: false, message: '文件重命名失败' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = `/uploads/avatars/${newFilename}`;
|
const avatarUrl = `/uploads/avatars/${newFilename}`;
|
||||||
res.json({ success: true, avatarUrl: avatarUrl });
|
res.json({ success: true, avatarUrl: avatarUrl });
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确保uploads目录可访问
|
// 确保uploads目录可访问
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user