Files
redlight/server/config/bbb.js
Michelle 89b2a853d3
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m20s
Build & Push Docker Image / build (release) Successful in 6m23s
Bump version to 1.2.0 in package.json, package-lock.json, and federation.js
2026-02-28 23:15:13 +01:00

227 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import crypto from 'crypto';
import xml2js from 'xml2js';
const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/';
const BBB_SECRET = process.env.BBB_SECRET || '';
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);
// Logging: compact key=value style, filter sensitive params
function formatUTC(d) {
const pad = n => String(n).padStart(2, '0');
const Y = d.getUTCFullYear();
const M = pad(d.getUTCMonth() + 1);
const D = pad(d.getUTCDate());
const h = pad(d.getUTCHours());
const m = pad(d.getUTCMinutes());
const s = pad(d.getUTCSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`;
}
const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i];
const isSensitive = key => SENSITIVE_KEYS.some(rx => rx.test(key));
function sanitizeParams(p) {
try {
const out = [];
for (const k of Object.keys(p || {})) {
if (k.toLowerCase() === 'checksum') continue; // never log checksum
if (isSensitive(k)) {
out.push(`${k}=[FILTERED]`);
} else {
let v = p[k];
if (typeof v === 'string' && v.length > 100) v = v.slice(0, 100) + '...[truncated]';
out.push(`${k}=${String(v)}`);
}
}
return out.join('&') || '-';
} catch (e) {
return '-';
}
}
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,
});
// Compact log: time=... method=GET path=getMeetings format=xml status=200 duration=12.34 bbb_returncode=SUCCESS params=meetingID=123
try {
const tokens = [];
tokens.push(`time=${formatUTC(new Date())}`);
tokens.push(`method=${xmlBody ? 'POST' : 'GET'}`);
// include standard BBB api base path
let apiBasePath = '/bigbluebutton/api';
try {
const u = new URL(BBB_URL);
apiBasePath = (u.pathname || '/bigbluebutton/api').replace(/\/$/, '');
} catch (e) {
// keep default
}
// ensure single slash separation
const fullPath = `${apiBasePath}/${apiCallName}`.replace(/\/\/+/, '/');
tokens.push(`path=${fullPath}`);
tokens.push(`format=xml`);
tokens.push(`status=${response.status}`);
tokens.push(`duration=${(duration).toFixed(2)}`);
const returnCode = result && result.response && result.response.returncode ? result.response.returncode : '-';
tokens.push(`returncode=${returnCode}`);
const safeParams = sanitizeParams(params);
tokens.push(`params=${safeParams}`);
console.info(tokens.join(' '));
} catch (e) {
// ignore logging errors
}
return result.response;
} catch (error) {
const duration = Date.now() - start;
console.error(`BBB API error (${apiCallName}) status=error duration=${(duration).toFixed(2)} err=${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) {
const { moderatorPW, attendeePW } = getRoomPasswords(room.uid);
// Build welcome message with guest invite link
let welcome = 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="${guestLink}">${guestLink}</a>`;
if (room.access_code) {
welcome += `<br>Access Code: <b>${room.access_code}</b>`;
}
}
const params = {
meetingID: room.uid,
name: room.name,
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';
}
// Build optional presentation XML body escape URL to prevent XML injection
let xmlBody = null;
if (presentationUrl) {
const safeUrl = presentationUrl
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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 };