diff --git a/client/jest.setup.js b/client/jest.setup.js
index 8101fdb..1fbdb2c 100644
--- a/client/jest.setup.js
+++ b/client/jest.setup.js
@@ -1,8 +1,8 @@
/* eslint-disable no-undef */
import fetch from 'node-fetch';
import { TextEncoder, TextDecoder } from 'util';
-import { PeerConnectionMock, SessionDescriptionMock, IceCandidateMock } from './test/peerconnectionmock';
-import ResizeObserverMock from './test/resizeobservermock';
+import { PeerConnectionMock, SessionDescriptionMock, IceCandidateMock } from './test/mocks/peerconnectionmock.js';
+import ResizeObserverMock from './test/helpers/resizeobservermock.js';
// note: If set testEnvironment `jest-environment-jsdom`, below classes are not defined.
@@ -32,4 +32,4 @@ if (!window.RTCIceCandidate) {
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserverMock;
-}
\ No newline at end of file
+}
diff --git a/client/public/call/chat/renderer-chat.js b/client/public/call/chat/renderer-chat.js
index c164ca1..7fcb4c2 100644
--- a/client/public/call/chat/renderer-chat.js
+++ b/client/public/call/chat/renderer-chat.js
@@ -1,3 +1,5 @@
+import { createTextElement, textValue } from '../../shared/dom.js';
+
export function createMessageElement(message, formatTimestamp) {
const messageDiv = document.createElement('div');
let messageClass = 'chat-bubble';
@@ -13,31 +15,45 @@ export function createMessageElement(message, formatTimestamp) {
messageDiv.className = messageClass;
messageDiv.dataset.messageId = message.id;
- const contentHTML = message.type === 'file' && message.content.startsWith('data:image/')
- ? `
-
-

- ${message.fileName ? `
${message.fileName}
` : ''}
-
- `
- : `
-
- ${message.content}
-
- `;
+ const header = document.createElement('div');
+ header.className = 'message-header';
- messageDiv.innerHTML = `
-
-
- ${contentHTML}
-
- `;
+ const avatar = document.createElement('img');
+ avatar.className = 'message-avatar';
+ avatar.src = textValue(message.senderAvatar);
+ avatar.alt = textValue(message.senderName, '\u7528\u6237');
+ header.appendChild(avatar);
+
+ const headerText = document.createElement('div');
+ headerText.appendChild(createTextElement('span', 'message-sender', message.senderName));
+ headerText.appendChild(createTextElement('span', 'message-time', formatTimestamp(message.timestamp)));
+ header.appendChild(headerText);
+
+ const content = document.createElement('div');
+ content.className = 'message-content';
+ const rawContent = textValue(message.content);
+
+ if (message.type === 'file' && rawContent.startsWith('data:image/')) {
+ const imageContainer = document.createElement('div');
+ imageContainer.className = 'message-image-container';
+
+ const image = document.createElement('img');
+ image.src = rawContent;
+ image.className = 'message-image';
+ image.alt = textValue(message.fileName, '\u56fe\u7247');
+ imageContainer.appendChild(image);
+
+ if (message.fileName) {
+ imageContainer.appendChild(createTextElement('div', 'message-image-name', message.fileName));
+ }
+
+ content.appendChild(imageContainer);
+ } else {
+ content.appendChild(createTextElement('div', 'message-text', rawContent));
+ }
+
+ messageDiv.appendChild(header);
+ messageDiv.appendChild(content);
return messageDiv;
}
diff --git a/client/public/call/participants/renderer-participant-grid.js b/client/public/call/participants/renderer-participant-grid.js
index 2d2b8f1..911f6ac 100644
--- a/client/public/call/participants/renderer-participant-grid.js
+++ b/client/public/call/participants/renderer-participant-grid.js
@@ -1,3 +1,5 @@
+import { createIconElement, createTextElement, textValue } from '../../shared/dom.js';
+
function createParticipantPlaceholder() {
const placeholder = document.createElement('div');
placeholder.className = 'participant-video-placeholder absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80 hidden';
@@ -15,32 +17,39 @@ function createParticipantPlaceholder() {
export function createParticipantTile(connectionId, displayName) {
const tile = document.createElement('div');
tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center';
- tile.dataset.participantId = connectionId;
+ tile.dataset.participantId = textValue(connectionId);
const video = document.createElement('video');
video.className = 'w-full h-full object-contain';
video.autoplay = true;
video.playsinline = true;
video.muted = false;
- video.id = `participantVideo_${connectionId}`;
+ video.id = `participantVideo_${textValue(connectionId)}`;
tile.appendChild(video);
tile.appendChild(createParticipantPlaceholder());
const label = document.createElement('div');
label.className = 'absolute bottom-3 left-3 glass px-3 py-1 rounded-full text-xs flex items-center gap-2';
- label.innerHTML = `${displayName || '\u53c2\u4e0e\u8005'}`;
+ label.appendChild(createIconElement('fas fa-user text-purple-400'));
+ label.appendChild(createTextElement('span', '', displayName, '\u53c2\u4e0e\u8005'));
tile.appendChild(label);
const liveTag = document.createElement('div');
liveTag.className = 'absolute top-3 right-3 bg-green-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1';
- liveTag.innerHTML = `\u5728\u7ebf`;
+ const pulse = document.createElement('span');
+ pulse.className = 'w-1.5 h-1.5 bg-white rounded-full animate-pulse';
+ liveTag.appendChild(pulse);
+ liveTag.appendChild(createTextElement('span', '', '\u5728\u7ebf'));
tile.appendChild(liveTag);
return tile;
}
export function getParticipantTile(grid, participantId) {
- return grid?.querySelector(`[data-participant-id="${participantId}"]`) || null;
+ if (!grid) return null;
+ const expectedId = textValue(participantId);
+ return Array.from(grid.querySelectorAll('[data-participant-id]'))
+ .find(tile => tile.dataset.participantId === expectedId) || null;
}
export function updateParticipantTilePlaceholder(grid, participantId, showPlaceholder) {
diff --git a/client/public/call/renderers/renderer-ui.js b/client/public/call/renderers/renderer-ui.js
index 6990cb6..b56705f 100644
--- a/client/public/call/renderers/renderer-ui.js
+++ b/client/public/call/renderers/renderer-ui.js
@@ -1,3 +1,5 @@
+import { createIconElement, createTextElement, textValue } from '../../shared/dom.js';
+
const DEFAULT_NETWORK_QUALITY = {
label: '\u672a\u77e5',
statusIconClass: 'fas fa-question-circle text-gray-400',
@@ -50,18 +52,18 @@ const NETWORK_QUALITY_DISPLAY = {
}
};
-function getRoleTagMarkup(user, role) {
+function getRoleTagMeta(user, role) {
if (role === 'local') {
return user.isHost
- ? '\u4e3b\u6301\u4eba'
- : '\u53c2\u4e0e\u8005';
+ ? { className: 'text-xs bg-indigo-500 px-1.5 rounded ml-1', label: '\u4e3b\u6301\u4eba' }
+ : { className: 'text-xs bg-purple-500 px-1.5 rounded ml-1', label: '\u53c2\u4e0e\u8005' };
}
if (role === 'participant') {
- return '\u53c2\u4e0e\u8005';
+ return { className: 'text-xs bg-purple-500 px-1.5 rounded ml-1', label: '\u53c2\u4e0e\u8005' };
}
- return '\u4e3b\u6301\u4eba';
+ return { className: 'text-xs bg-indigo-500 px-1.5 rounded ml-1', label: '\u4e3b\u6301\u4eba' };
}
function getDatasetUserId(role, id) {
@@ -79,34 +81,55 @@ function getDatasetUserId(role, id) {
}
}
-function getAvatarMarkup(user, role) {
- if (role === 'local') {
- return `
`;
- }
-
- return `
-
-

-
-
- `;
+function createAvatarImage(user) {
+ const image = document.createElement('img');
+ image.src = textValue(user.avatar);
+ image.alt = textValue(user.name, '\u7528\u6237');
+ image.className = 'w-10 h-10 rounded-full object-cover';
+ return image;
}
-function getRightMarkup(mediaState, role, muteIconMarkup) {
- if (role !== 'participant') {
- return muteIconMarkup;
+function createAvatarElement(user, role) {
+ if (role === 'local') {
+ return createAvatarImage(user);
}
- const speakingMarkup = (mediaState.isSpeaking && mediaState.audio)
- ? '
'
- : '';
+ const wrapper = document.createElement('div');
+ wrapper.className = 'relative';
+ wrapper.appendChild(createAvatarImage(user));
- return `
-
- ${muteIconMarkup}
- ${speakingMarkup}
-
- `;
+ const statusDot = document.createElement('div');
+ statusDot.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900';
+ wrapper.appendChild(statusDot);
+
+ return wrapper;
+}
+
+function createAudioWaveElement() {
+ const wave = document.createElement('div');
+ wave.className = 'audio-wave w-6';
+ for (let i = 0; i < 5; i += 1) {
+ wave.appendChild(document.createElement('span'));
+ }
+ return wave;
+}
+
+function createRightElement(mediaState, role, muteIcon) {
+ if (role !== 'participant') {
+ return muteIcon;
+ }
+
+ const right = document.createElement('div');
+ right.className = 'flex items-center gap-2';
+
+ if (muteIcon) {
+ right.appendChild(muteIcon);
+ }
+ if (mediaState.isSpeaking && mediaState.audio) {
+ right.appendChild(createAudioWaveElement());
+ }
+
+ return right.childNodes.length > 0 ? right : null;
}
export function getCallTitle(connectionId) {
@@ -163,27 +186,40 @@ export function buildUserCountLabel(userCount) {
export function createUserEntryElement({ user, role, id }) {
const entry = document.createElement('div');
const mediaMeta = getMediaStatusMeta(user.mediaState);
- const muteIconMarkup = mediaMeta.showMuteIcon
- ? ``
+ const muteIcon = mediaMeta.showMuteIcon
+ ? createIconElement(mediaMeta.muteIconClass)
: '';
const baseClass = 'flex items-center gap-3 p-2 rounded-lg';
- const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : '';
entry.className = role === 'local'
? `${baseClass} hover:bg-white/5`
: `${baseClass} bg-white/5`;
entry.dataset.userId = getDatasetUserId(role, id);
- entry.innerHTML = `
- ${getAvatarMarkup(user, role)}
-
-
- ${user.name}
- ${getRoleTagMarkup(user, role)}
-
-
${mediaMeta.text}
-
- ${getRightMarkup(user.mediaState, role, muteIconMarkup)}
- `;
+
+ entry.appendChild(createAvatarElement(user, role));
+
+ const details = document.createElement('div');
+ details.className = 'flex-1';
+
+ const nameRow = document.createElement('div');
+ nameRow.className = 'text-sm font-medium';
+ nameRow.appendChild(document.createTextNode(textValue(user.name)));
+ const roleTag = getRoleTagMeta(user, role);
+ nameRow.appendChild(createTextElement('span', roleTag.className, roleTag.label));
+ details.appendChild(nameRow);
+
+ const mediaStatus = createTextElement('div', mediaMeta.className, mediaMeta.text);
+ if (role === 'local') {
+ mediaStatus.dataset.field = 'localUser.mediaStatus';
+ }
+ details.appendChild(mediaStatus);
+
+ entry.appendChild(details);
+
+ const right = createRightElement(user.mediaState, role, muteIcon || null);
+ if (right) {
+ entry.appendChild(right);
+ }
return entry;
}
diff --git a/client/public/call/signaling/connect-directory.js b/client/public/call/signaling/connect-directory.js
index 5036797..eefb19f 100644
--- a/client/public/call/signaling/connect-directory.js
+++ b/client/public/call/signaling/connect-directory.js
@@ -1,3 +1,5 @@
+import { createTextElement, textValue } from '../../shared/dom.js';
+
const EMPTY_CONNECTION_IDS_HTML = '\u6682\u65e0\u53ef\u7528\u7684\u8fde\u63a5ID
';
const EMPTY_USERS_HTML = '\u6682\u65e0\u5728\u7ebf\u7528\u6237
';
const HALL_LABEL = '\u5927\u5385\uff08\u672a\u52a0\u5165\u623f\u95f4\uff09';
@@ -10,13 +12,14 @@ const SELECT_LABEL = '\u9009\u62e9';
const USER_COUNT_SUFFIX = '\u4eba';
const ONLINE_USERS_SUMMARY_SUFFIX = ' \u4e2aWebSocket\u7528\u6237\u5728\u7ebf';
-function escapeHtml(value) {
- return String(value || '')
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
+function getRoleTagClass(role) {
+ if (role === 'host') {
+ return 'text-xs px-2 py-1 rounded-full bg-indigo-500/20 text-indigo-300';
+ }
+ if (role === 'participant') {
+ return 'text-xs px-2 py-1 rounded-full bg-white/10 text-gray-300';
+ }
+ return 'text-xs px-2 py-1 rounded-full bg-emerald-500/20 text-emerald-300';
}
export async function fetchOnlineUsers() {
@@ -115,10 +118,8 @@ export function renderOnlineUsers({ users, currentUserId, onlineUsersList, users
const roomTitle = document.createElement('div');
roomTitle.className = 'flex items-center justify-between mb-2';
- roomTitle.innerHTML = `
- ${escapeHtml(groupName)}
- ${roomUsers.length} ${USER_COUNT_SUFFIX}
- `;
+ roomTitle.appendChild(createTextElement('span', 'text-sm font-medium text-white', groupName));
+ roomTitle.appendChild(createTextElement('span', 'text-xs text-gray-400', `${roomUsers.length} ${USER_COUNT_SUFFIX}`));
section.appendChild(roomTitle);
const roomList = document.createElement('div');
@@ -135,19 +136,31 @@ export function renderOnlineUsers({ users, currentUserId, onlineUsersList, users
const userItem = document.createElement('div');
userItem.className = 'flex items-center justify-between rounded-lg bg-black/20 px-3 py-2';
- userItem.innerHTML = `
-
-
})
-
-
${escapeHtml(userName)}
-
${escapeHtml(identity)}
-
-
-
- ${roleLabel}
- ${isSelf ? `${SELF_LABEL}` : ''}
-
- `;
+
+ const profile = document.createElement('div');
+ profile.className = 'flex items-center gap-3 min-w-0';
+
+ const avatarImage = document.createElement('img');
+ avatarImage.src = textValue(avatar);
+ avatarImage.alt = textValue(userName);
+ avatarImage.className = 'w-8 h-8 rounded-full object-cover';
+ profile.appendChild(avatarImage);
+
+ const info = document.createElement('div');
+ info.className = 'min-w-0';
+ info.appendChild(createTextElement('div', 'text-sm text-white truncate', userName));
+ info.appendChild(createTextElement('div', 'text-xs text-gray-400 truncate', identity));
+ profile.appendChild(info);
+
+ const status = document.createElement('div');
+ status.className = 'flex items-center gap-2';
+ status.appendChild(createTextElement('span', getRoleTagClass(user.role), roleLabel));
+ if (isSelf) {
+ status.appendChild(createTextElement('span', 'text-xs text-gray-500', SELF_LABEL));
+ }
+
+ userItem.appendChild(profile);
+ userItem.appendChild(status);
roomList.appendChild(userItem);
});
diff --git a/client/public/shared/dom.js b/client/public/shared/dom.js
new file mode 100644
index 0000000..7aebc87
--- /dev/null
+++ b/client/public/shared/dom.js
@@ -0,0 +1,18 @@
+export function textValue(value, fallback = '') {
+ return value == null || value === '' ? fallback : String(value);
+}
+
+export function createTextElement(tagName, className, value, fallback = '') {
+ const element = document.createElement(tagName);
+ if (className) {
+ element.className = className;
+ }
+ element.textContent = textValue(value, fallback);
+ return element;
+}
+
+export function createIconElement(className) {
+ const icon = document.createElement('i');
+ icon.className = className;
+ return icon;
+}
diff --git a/client/src/core/signaling.js b/client/src/core/signaling.js
index 413e34f..b6536db 100644
--- a/client/src/core/signaling.js
+++ b/client/src/core/signaling.js
@@ -2,10 +2,11 @@ import * as Logger from "../utils/logger.js";
export class Signaling extends EventTarget {
- constructor(interval = 1000) {
+ constructor(interval = 1000, baseUrl = null) {
super();
this.running = false;
this.interval = interval;
+ this.baseUrl = baseUrl;
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
}
@@ -19,7 +20,7 @@ export class Signaling extends EventTarget {
}
url(method, parameter = '') {
- let ret = location.origin + '/signaling';
+ let ret = (this.baseUrl || location.origin) + '/signaling';
if (method)
ret += '/' + method;
if (parameter)
@@ -151,16 +152,17 @@ export class Signaling extends EventTarget {
export class WebSocketSignaling extends EventTarget {
- constructor(interval = 1000) {
+ constructor(interval = 1000, websocketUrl = null) {
super();
this.interval = interval;
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
- let websocketUrl;
- if (location.protocol === "https:") {
- websocketUrl = "wss://" + location.host;
- } else {
- websocketUrl = "ws://" + location.host;
+ if (!websocketUrl) {
+ if (location.protocol === "https:") {
+ websocketUrl = "wss://" + location.host;
+ } else {
+ websocketUrl = "ws://" + location.host;
+ }
}
this.websocket = new WebSocket(websocketUrl);
diff --git a/client/test/unit/meeting-recorder.test.js b/client/test/unit/meeting-recorder.test.js
index 4029f49..4443d76 100644
--- a/client/test/unit/meeting-recorder.test.js
+++ b/client/test/unit/meeting-recorder.test.js
@@ -1,3 +1,4 @@
+import { jest } from '@jest/globals';
import { MeetingRecorder } from '../../public/call/media/meeting-recorder.js';
class MediaStreamMock {
@@ -107,7 +108,7 @@ describe('MeetingRecorder', () => {
const result = await recorder.stop();
expect(result.filename).toContain('meeting-recording-123-456-789');
- expect(result.mimeType).toBe('video/mp4;codecs=avc1.42E01E,mp4a.40.2');
+ expect(result.mimeType.toLowerCase()).toBe('video/mp4;codecs=avc1.42e01e,mp4a.40.2');
expect(result.filename).toMatch(/\.mp4$/);
expect(windowRef.URL.createObjectURL).not.toHaveBeenCalled();
expect(recorder.isRecording()).toBe(false);
diff --git a/client/test/unit/rendering-safety.test.js b/client/test/unit/rendering-safety.test.js
new file mode 100644
index 0000000..f1bb86a
--- /dev/null
+++ b/client/test/unit/rendering-safety.test.js
@@ -0,0 +1,88 @@
+import { createMessageElement } from '../../public/call/chat/renderer-chat.js';
+import { createParticipantTile, getParticipantTile } from '../../public/call/participants/renderer-participant-grid.js';
+import { createUserEntryElement } from '../../public/call/renderers/renderer-ui.js';
+import { renderOnlineUsers } from '../../public/call/signaling/connect-directory.js';
+
+const formatTimestamp = value => value;
+const unsafeText = '
Alice';
+
+function mediaState(overrides = {}) {
+ return {
+ audio: true,
+ video: true,
+ isSpeaking: false,
+ ...overrides
+ };
+}
+
+describe('safe dynamic rendering', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ test('renders chat text as text, not markup', () => {
+ const element = createMessageElement({
+ id: 'msg-1',
+ type: 'text',
+ isSelf: false,
+ senderName: unsafeText,
+ senderAvatar: '/images/p1.png',
+ content: unsafeText,
+ timestamp: 'now'
+ }, formatTimestamp);
+
+ expect(element.querySelector('.message-text').textContent).toBe(unsafeText);
+ expect(element.querySelector('.message-content img')).toBeNull();
+ expect(element.querySelector('.message-sender').textContent).toBe(unsafeText);
+ });
+
+ test('renders participant names safely and finds ids without selector injection', () => {
+ const participantId = 'room"] [data-bad="1';
+ const tile = createParticipantTile(participantId, unsafeText);
+ const grid = document.createElement('div');
+ grid.appendChild(tile);
+
+ expect(tile.querySelector('.absolute.bottom-3 span').textContent).toBe(unsafeText);
+ expect(tile.querySelector('.absolute.bottom-3 img')).toBeNull();
+ expect(getParticipantTile(grid, participantId)).toBe(tile);
+ });
+
+ test('renders user list entries without interpreting user profile fields as HTML', () => {
+ const entry = createUserEntryElement({
+ role: 'participant',
+ id: 'participant-1',
+ user: {
+ name: unsafeText,
+ avatar: '/images/p2.png',
+ mediaState: mediaState({ audio: false })
+ }
+ });
+
+ expect(entry.textContent).toContain(unsafeText);
+ expect(entry.querySelectorAll('img')).toHaveLength(1);
+ });
+
+ test('renders online users without injecting markup from directory data', () => {
+ const onlineUsersList = document.createElement('div');
+ const usersContainer = document.createElement('div');
+ const onlineUsersSummary = document.createElement('div');
+
+ renderOnlineUsers({
+ users: [{
+ name: unsafeText,
+ userId: unsafeText,
+ avatar: '/images/p1.png',
+ role: 'participant',
+ connectionId: 'room-1'
+ }],
+ currentUserId: 'other-user',
+ onlineUsersList,
+ usersContainer,
+ onlineUsersSummary
+ });
+
+ expect(usersContainer.textContent).toContain(unsafeText);
+ expect(usersContainer.querySelector('button')).toBeNull();
+ expect(usersContainer.querySelectorAll('img')).toHaveLength(1);
+ });
+});
diff --git a/client/test/unit/signaling.test.js b/client/test/unit/signaling.test.js
index 98a1bf8..efb776e 100644
--- a/client/test/unit/signaling.test.js
+++ b/client/test/unit/signaling.test.js
@@ -1,18 +1,49 @@
import { jest } from '@jest/globals';
+import fs from 'fs';
import * as Path from 'path';
+import process from 'process';
import { setup, teardown } from 'jest-dev-server';
import { Signaling, WebSocketSignaling } from "../../src/core/signaling.js";
import { MockSignaling, reset } from "../mocks/mocksignaling.js";
import { waitFor, sleep, serverExeName } from "../helpers/testutils.js";
const portNumber = 8081;
+const runSignalingIntegration = process.env.RUN_SIGNALING_INTEGRATION === '1';
+const signalingModes = runSignalingIntegration
+ ? [{ mode: "mock" }, { mode: "http" }, { mode: "websocket" }]
+ : [{ mode: "mock" }];
+
jest.setTimeout(10000);
-describe.each([
- { mode: "mock" },
- { mode: "http" },
- { mode: "websocket" },
-])('signaling test in public mode', ({ mode }) => {
+function buildServerCommand(args = '') {
+ const binaryPath = Path.resolve(`../bin~/${serverExeName()}`);
+ const buildEntryPath = Path.resolve('../build/index.js');
+ const serverCommand = fs.existsSync(binaryPath)
+ ? `"${binaryPath}"`
+ : `"${process.execPath}" "${buildEntryPath}"`;
+
+ return `${serverCommand} ${args}`.trim();
+}
+
+function getPortForMode(mode, isPrivate) {
+ if (mode === 'mock') {
+ return portNumber;
+ }
+
+ const publicPorts = { http: portNumber + 1, websocket: portNumber + 2 };
+ const privatePorts = { http: portNumber + 3, websocket: portNumber + 4 };
+ return (isPrivate ? privatePorts : publicPorts)[mode];
+}
+
+function createHttpSignaling(port) {
+ return new Signaling(1, `http://localhost:${port}`);
+}
+
+function createWebSocketSignaling(port) {
+ return new WebSocketSignaling(1, `ws://localhost:${port}`);
+}
+
+describe.each(signalingModes)('signaling test in public mode', ({ mode }) => {
let signaling1;
let signaling2;
const connectionId1 = "12345";
@@ -26,22 +57,22 @@ describe.each([
signaling1 = new MockSignaling(1);
signaling2 = new MockSignaling(1);
} else {
- const path = Path.resolve(`../bin~/${serverExeName()}`);
- let cmd = `${path} -p ${portNumber}`;
+ const serverPort = getPortForMode(mode, false);
+ let cmd = buildServerCommand(`-p ${serverPort}`);
if (mode == "http") {
cmd += " -t http";
}
- await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
+ await setup({ command: cmd, port: serverPort, usedPortAction: 'error' });
if (mode == "http") {
- signaling1 = new Signaling(1);
- signaling2 = new Signaling(1);
+ signaling1 = createHttpSignaling(serverPort);
+ signaling2 = createHttpSignaling(serverPort);
}
if (mode == "websocket") {
- signaling1 = new WebSocketSignaling(1);
- signaling2 = new WebSocketSignaling(1);
+ signaling1 = createWebSocketSignaling(serverPort);
+ signaling2 = createWebSocketSignaling(serverPort);
}
}
@@ -50,8 +81,8 @@ describe.each([
});
afterAll(async () => {
- await signaling1.stop();
- await signaling2.stop();
+ await signaling1?.stop();
+ await signaling2?.stop();
signaling1 = null;
signaling2 = null;
@@ -207,11 +238,7 @@ describe.each([
});
});
-describe.each([
- { mode: "mock" },
- { mode: "http" },
- { mode: "websocket" },
-])('signaling test in private mode', ({ mode }) => {
+describe.each(signalingModes)('signaling test in private mode', ({ mode }) => {
let signaling1;
let signaling2;
const connectionId = "12345";
@@ -226,22 +253,22 @@ describe.each([
return;
}
- const path = Path.resolve(`../bin~/${serverExeName()}`);
- let cmd = `${path} -p ${portNumber} -m private`;
+ const serverPort = getPortForMode(mode, true);
+ let cmd = buildServerCommand(`-p ${serverPort} -m private`);
if (mode == "http") {
cmd += " -t http";
}
- await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
+ await setup({ command: cmd, port: serverPort, usedPortAction: 'error' });
if (mode == "http") {
- signaling1 = new Signaling(1);
- signaling2 = new Signaling(1);
+ signaling1 = createHttpSignaling(serverPort);
+ signaling2 = createHttpSignaling(serverPort);
}
if (mode == "websocket") {
- signaling1 = new WebSocketSignaling(1);
- signaling2 = new WebSocketSignaling(1);
+ signaling1 = createWebSocketSignaling(serverPort);
+ signaling2 = createWebSocketSignaling(serverPort);
}
await signaling1.start();
@@ -249,8 +276,8 @@ describe.each([
});
afterAll(async () => {
- await signaling1.stop();
- await signaling2.stop();
+ await signaling1?.stop();
+ await signaling2?.stop();
signaling1 = null;
signaling2 = null;
diff --git a/src/class/httphandler.ts b/src/class/httphandler.ts
index 1ee3b7a..b2a05ec 100644
--- a/src/class/httphandler.ts
+++ b/src/class/httphandler.ts
@@ -7,7 +7,7 @@ import Offer from './offer';
import Answer from './answer';
import Candidate from './candidate';
import { v4 as uuid } from 'uuid';
-import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers, onGetRooms as onGetWsRooms } from './websockethandler';
+import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers, } from './websockethandler';
import { log, LogLevel } from '../log';
/**
* 断开连接记录类
@@ -996,57 +996,6 @@ function postCandidate(req: Request, res: Response): void {
arr.push(candidate);
res.sendStatus(200);
}
-/**
- * @swagger
- * /signaling/rooms:
- * get:
- * summary: 获取房间和用户信息
- * description: 获取所有房间的信息,包括房间ID和链接的用户
- * security:
- * - sessionAuth: []
- * responses:
- * 200:
- * description: 成功获取房间和用户信息
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * rooms:
- * type: array
- * items:
- * type: object
- * properties:
- * roomId:
- * type: string
- * description: 房间ID
- * users:
- * type: array
- * items:
- * type: object
- * properties:
- * sessionId:
- * type: string
- * description: 会话ID
- * connected:
- * type: boolean
- * description: 连接状态
- * userCount:
- * type: number
- * description: 用户数量
- * totalRooms:
- * type: number
- * description: 总房间数
- */
-function onGetConnections(req: Request, res: Response): void {
- const connectionId = typeof req.query.connectionId === 'string' ? req.query.connectionId : undefined;
- const wsRooms = onGetWsRooms(connectionId).map((room) => ({
- ...room,
- users: room.members
- }));
-
- res.json({ rooms: wsRooms, totalRooms: wsRooms.length });
-}
/**
* @swagger
@@ -1160,7 +1109,6 @@ export {
postOffer, // 处理offer信令消息
postAnswer, // 处理answer信令消息
postCandidate, // 处理candidate信令消息
- onGetConnections, // 获取房间和用户信息
getAllConnectionIds, // 获取所有连接ID
getOnlineUsers // 获取在线WebSocket用户列表
};
diff --git a/src/signaling.ts b/src/signaling.ts
index 5671bab..c5486d5 100644
--- a/src/signaling.ts
+++ b/src/signaling.ts
@@ -6,7 +6,7 @@ const router: express.Router = express.Router();
// 不需要会话ID的路由
router.get('/connection-ids', handler.getAllConnectionIds);
router.get('/users', handler.getOnlineUsers);
-router.get('/rooms', handler.onGetConnections);
+// router.get('/rooms', handler.onGetConnections);
// 需要会话ID的路由
router.use(handler.checkSessionId);