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 || '\u56fe\u7247'} - ${message.fileName ? `
${message.fileName}
` : ''} -
- ` - : ` -
- ${message.content} -
- `; + const header = document.createElement('div'); + header.className = 'message-header'; - messageDiv.innerHTML = ` -
- -
- ${message.senderName} - ${formatTimestamp(message.timestamp)} -
-
-
- ${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(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);