All checks were successful
Build & Push Docker Image / build (push) Successful in 6m33s
195 lines
6.4 KiB
JavaScript
195 lines
6.4 KiB
JavaScript
import crypto from 'crypto';
|
|
import xml2js from 'xml2js';
|
|
import { log, fmtDuration, fmtStatus, fmtMethod, fmtReturncode, sanitizeBBBParams } from './logger.js';
|
|
|
|
const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/';
|
|
const BBB_SECRET = process.env.BBB_SECRET || '';
|
|
|
|
if (!BBB_SECRET) {
|
|
log.bbb.warn('WARNING: BBB_SECRET is not set. BBB API calls will use an empty secret.');
|
|
}
|
|
|
|
// HTML-escape for safe embedding in BBB welcome messages
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function getChecksum(apiCall, params) {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
const raw = apiCall + queryString + BBB_SECRET;
|
|
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
}
|
|
|
|
function buildUrl(apiCall, params = {}) {
|
|
const checksum = getChecksum(apiCall, params);
|
|
const queryString = new URLSearchParams({ ...params, checksum }).toString();
|
|
return `${BBB_URL}${apiCall}?${queryString}`;
|
|
}
|
|
|
|
async function apiCall(apiCallName, params = {}, xmlBody = null) {
|
|
const url = buildUrl(apiCallName, params);
|
|
const method = xmlBody ? 'POST' : 'GET';
|
|
|
|
const start = Date.now();
|
|
try {
|
|
const fetchOptions = xmlBody
|
|
? { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlBody }
|
|
: {};
|
|
const response = await fetch(url, fetchOptions);
|
|
const duration = Date.now() - start;
|
|
const xml = await response.text();
|
|
const result = await xml2js.parseStringPromise(xml, {
|
|
explicitArray: false,
|
|
trim: true,
|
|
});
|
|
|
|
const returncode = result?.response?.returncode || '-';
|
|
const paramStr = sanitizeBBBParams(params);
|
|
|
|
// Greenlight-style: method action → status returncode (duration) params
|
|
log.bbb.info(
|
|
`${fmtMethod(method)} ${apiCallName} → ${fmtStatus(response.status)} ${fmtReturncode(returncode)} (${fmtDuration(duration)}) ${paramStr}`
|
|
);
|
|
|
|
return result.response;
|
|
} catch (error) {
|
|
const duration = Date.now() - start;
|
|
log.bbb.error(
|
|
`${fmtMethod(method)} ${apiCallName} ✗ FAILED (${fmtDuration(duration)}) ${error.message}`
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Generate deterministic passwords from room UID
|
|
function getRoomPasswords(uid) {
|
|
const modPw = crypto.createHash('sha256').update(uid + '_mod_' + BBB_SECRET).digest('hex').substring(0, 16);
|
|
const attPw = crypto.createHash('sha256').update(uid + '_att_' + BBB_SECRET).digest('hex').substring(0, 16);
|
|
return { moderatorPW: modPw, attendeePW: attPw };
|
|
}
|
|
|
|
export async function createMeeting(room, logoutURL, loginURL = null, presentationUrl = null, analyticsCallbackURL = null) {
|
|
const { moderatorPW, attendeePW } = getRoomPasswords(room.uid);
|
|
|
|
// Build welcome message with guest invite link
|
|
// HTML-escape user-controlled content to prevent stored XSS via BBB
|
|
let welcome = room.welcome_message ? escapeHtml(room.welcome_message) : t('defaultWelcome');
|
|
if (logoutURL) {
|
|
const guestLink = `${logoutURL}/join/${room.uid}`;
|
|
welcome += `<br><br>To invite other participants, share this link:<br><a href="${escapeHtml(guestLink)}">${escapeHtml(guestLink)}</a>`;
|
|
// Access code is intentionally NOT shown in the welcome message to prevent
|
|
// leaking it to all meeting participants.
|
|
}
|
|
|
|
const params = {
|
|
meetingID: room.uid,
|
|
name: room.name.length >= 2 ? room.name : room.name.padEnd(2, ' '),
|
|
attendeePW,
|
|
moderatorPW,
|
|
welcome,
|
|
record: room.record_meeting ? 'true' : 'false',
|
|
autoStartRecording: 'false',
|
|
allowStartStopRecording: 'true',
|
|
muteOnStart: room.mute_on_join ? 'true' : 'false',
|
|
'meta_bbb-origin': 'Redlight',
|
|
'meta_bbb-origin-server-name': 'Redlight',
|
|
};
|
|
if (logoutURL) {
|
|
params.logoutURL = logoutURL;
|
|
}
|
|
if (loginURL) {
|
|
params.loginURL = loginURL;
|
|
}
|
|
if (room.max_participants > 0) {
|
|
params.maxParticipants = room.max_participants.toString();
|
|
}
|
|
if (room.access_code) {
|
|
params.lockSettingsLockOnJoin = 'true';
|
|
}
|
|
if (analyticsCallbackURL) {
|
|
params['meta_analytics-callback-url'] = analyticsCallbackURL;
|
|
}
|
|
|
|
// Build optional presentation XML body - escape URL to prevent XML injection
|
|
let xmlBody = null;
|
|
if (presentationUrl) {
|
|
const safeUrl = presentationUrl
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
xmlBody = `<modules><module name="presentation"><document url="${safeUrl}" /></module></modules>`;
|
|
}
|
|
|
|
return apiCall('create', params, xmlBody);
|
|
}
|
|
|
|
export async function joinMeeting(uid, name, isModerator = false, avatarURL = null) {
|
|
const { moderatorPW, attendeePW } = getRoomPasswords(uid);
|
|
const params = {
|
|
meetingID: uid,
|
|
fullName: name,
|
|
password: isModerator ? moderatorPW : attendeePW,
|
|
redirect: 'true',
|
|
};
|
|
if (avatarURL) {
|
|
params.avatarURL = avatarURL;
|
|
}
|
|
return buildUrl('join', params);
|
|
}
|
|
|
|
export async function endMeeting(uid) {
|
|
const { moderatorPW } = getRoomPasswords(uid);
|
|
return apiCall('end', { meetingID: uid, password: moderatorPW });
|
|
}
|
|
|
|
export async function getMeetingInfo(uid) {
|
|
return apiCall('getMeetingInfo', { meetingID: uid });
|
|
}
|
|
|
|
export async function isMeetingRunning(uid) {
|
|
const result = await apiCall('isMeetingRunning', { meetingID: uid });
|
|
return result.running === 'true';
|
|
}
|
|
|
|
export async function getMeetings() {
|
|
return apiCall('getMeetings', {});
|
|
}
|
|
|
|
export async function getRecordings(meetingID) {
|
|
const params = meetingID ? { meetingID } : {};
|
|
const result = await apiCall('getRecordings', params);
|
|
if (result.returncode !== 'SUCCESS' || !result.recordings) {
|
|
return [];
|
|
}
|
|
const recordings = result.recordings.recording;
|
|
if (!recordings) return [];
|
|
return Array.isArray(recordings) ? recordings : [recordings];
|
|
}
|
|
|
|
export async function getRecordingByRecordId(recordID) {
|
|
const result = await apiCall('getRecordings', { recordID });
|
|
if (result.returncode !== 'SUCCESS' || !result.recordings) {
|
|
return null;
|
|
}
|
|
const recordings = result.recordings.recording;
|
|
if (!recordings) return null;
|
|
const arr = Array.isArray(recordings) ? recordings : [recordings];
|
|
return arr[0] || null;
|
|
}
|
|
|
|
export async function deleteRecording(recordID) {
|
|
return apiCall('deleteRecordings', { recordID });
|
|
}
|
|
|
|
export async function publishRecording(recordID, publish) {
|
|
return apiCall('publishRecordings', { recordID, publish: publish ? 'true' : 'false' });
|
|
}
|
|
|
|
export { getRoomPasswords };
|