All checks were successful
Build & Push Docker Image / build (push) Successful in 6m28s
217 lines
7.1 KiB
JavaScript
217 lines
7.1 KiB
JavaScript
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'}`);
|
||
tokens.push(`path=${apiCallName}`);
|
||
tokens.push(`format=xml`);
|
||
tokens.push(`status=${response.status}`);
|
||
tokens.push(`duration=${(duration).toFixed(2)}`);
|
||
const bbbCode = result && result.response && result.response.returncode ? result.response.returncode : '-';
|
||
tokens.push(`bbb_returncode=${bbbCode}`);
|
||
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, '&')
|
||
.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 };
|