1- import { createHash , randomUUID } from "node:crypto" ;
1+ import { createHash , createHmac , randomUUID } from "node:crypto" ;
22import { WEBAPP_URL } from "@calcom/lib/constants" ;
33import type { CalendarEvent } from "@calcom/types/Calendar" ;
44import type { PartialReference } from "@calcom/types/EventManager" ;
@@ -12,8 +12,7 @@ type BigBlueButtonKeys = {
1212 bigBlueButtonSharedSecret : string ;
1313} ;
1414
15- const ATTENDEE_PASSWORD = "attendee" ;
16- const MODERATOR_PASSWORD = "moderator" ;
15+ const BIGBLUEBUTTON_API_TIMEOUT_MS = 10_000 ;
1716
1817/**
1918 * Reads and validates the configured BigBlueButton server URL and shared secret.
@@ -26,9 +25,12 @@ const getBigBlueButtonKeys = async (): Promise<BigBlueButtonKeys> =>
2625 */
2726const normalizeServerUrl = ( serverUrl : string ) : URL => {
2827 const url = new URL ( serverUrl ) ;
28+ const trimmedPathname = url . pathname . replace ( / \/ + $ / , "" ) ;
2929
30- if ( ! url . pathname . endsWith ( "/api/" ) ) {
31- url . pathname = `${ url . pathname . replace ( / \/ $ / , "" ) } /api/` ;
30+ if ( trimmedPathname . endsWith ( "/api" ) ) {
31+ url . pathname = `${ trimmedPathname } /` ;
32+ } else {
33+ url . pathname = `${ trimmedPathname } /api/` ;
3234 }
3335
3436 return url ;
@@ -40,6 +42,19 @@ const normalizeServerUrl = (serverUrl: string): URL => {
4042const createChecksum = ( callName : string , query : string , sharedSecret : string ) : string =>
4143 createHash ( "sha1" ) . update ( `${ callName } ${ query } ${ sharedSecret } ` ) . digest ( "hex" ) ;
4244
45+ /**
46+ * Derives stable per-meeting BigBlueButton passwords without storing moderator secrets.
47+ */
48+ const createMeetingPassword = ( {
49+ meetingID,
50+ role,
51+ sharedSecret,
52+ } : {
53+ meetingID : string ;
54+ role : "attendee" | "moderator" ;
55+ sharedSecret : string ;
56+ } ) : string => createHmac ( "sha256" , sharedSecret ) . update ( `${ meetingID } :${ role } ` ) . digest ( "hex" ) . slice ( 0 , 32 ) ;
57+
4358/**
4459 * Creates a signed BigBlueButton API URL for the given call and parameters.
4560 */
@@ -68,8 +83,24 @@ const createApiUrl = ({
6883 * Calls BigBlueButton and treats non-success API responses as failures.
6984 */
7085const callBigBlueButtonApi = async ( url : string ) : Promise < void > => {
71- const response = await fetch ( url ) ;
72- const body = await response . text ( ) ;
86+ const controller = new AbortController ( ) ;
87+ const timeout = setTimeout ( ( ) => controller . abort ( ) , BIGBLUEBUTTON_API_TIMEOUT_MS ) ;
88+
89+ let response : Response ;
90+ let body : string ;
91+
92+ try {
93+ response = await fetch ( url , { signal : controller . signal } ) ;
94+ body = await response . text ( ) ;
95+ } catch ( error ) {
96+ if ( error instanceof Error && error . name === "AbortError" ) {
97+ throw new Error ( "BigBlueButton API request timed out" ) ;
98+ }
99+
100+ throw error ;
101+ } finally {
102+ clearTimeout ( timeout ) ;
103+ }
73104
74105 if ( ! response . ok || ! body . includes ( "<returncode>SUCCESS</returncode>" ) ) {
75106 throw new Error ( "BigBlueButton API request failed" ) ;
@@ -95,6 +126,16 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
95126 ) : Promise < VideoCallData > => {
96127 const { bigBlueButtonServerUrl, bigBlueButtonSharedSecret } = await getBigBlueButtonKeys ( ) ;
97128 const meetingID = getMeetingId ( eventData , bookingRef ) ;
129+ const attendeePassword = createMeetingPassword ( {
130+ meetingID,
131+ role : "attendee" ,
132+ sharedSecret : bigBlueButtonSharedSecret ,
133+ } ) ;
134+ const moderatorPassword = createMeetingPassword ( {
135+ meetingID,
136+ role : "moderator" ,
137+ sharedSecret : bigBlueButtonSharedSecret ,
138+ } ) ;
98139
99140 await callBigBlueButtonApi (
100141 createApiUrl ( {
@@ -104,8 +145,8 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
104145 params : {
105146 name : eventData . title ,
106147 meetingID,
107- attendeePW : ATTENDEE_PASSWORD ,
108- moderatorPW : MODERATOR_PASSWORD ,
148+ attendeePW : attendeePassword ,
149+ moderatorPW : moderatorPassword ,
109150 logoutURL : WEBAPP_URL ,
110151 } ,
111152 } )
@@ -114,15 +155,15 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
114155 return {
115156 type : metadata . type ,
116157 id : meetingID ,
117- password : ATTENDEE_PASSWORD ,
158+ password : attendeePassword ,
118159 url : createApiUrl ( {
119160 callName : "join" ,
120161 serverUrl : bigBlueButtonServerUrl ,
121162 sharedSecret : bigBlueButtonSharedSecret ,
122163 params : {
123164 fullName : eventData . attendees [ 0 ] ?. name || "Guest" ,
124165 meetingID,
125- password : ATTENDEE_PASSWORD ,
166+ password : attendeePassword ,
126167 redirect : "true" ,
127168 } ,
128169 } ) ,
@@ -136,6 +177,11 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
136177 createOrUpdateMeeting ( eventData , bookingRef ) ,
137178 deleteMeeting : async ( meetingID : string ) : Promise < void > => {
138179 const { bigBlueButtonServerUrl, bigBlueButtonSharedSecret } = await getBigBlueButtonKeys ( ) ;
180+ const moderatorPassword = createMeetingPassword ( {
181+ meetingID,
182+ role : "moderator" ,
183+ sharedSecret : bigBlueButtonSharedSecret ,
184+ } ) ;
139185
140186 await callBigBlueButtonApi (
141187 createApiUrl ( {
@@ -144,7 +190,7 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
144190 sharedSecret : bigBlueButtonSharedSecret ,
145191 params : {
146192 meetingID,
147- password : MODERATOR_PASSWORD ,
193+ password : moderatorPassword ,
148194 } ,
149195 } )
150196 ) ;
@@ -155,10 +201,12 @@ const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
155201export const testHelpers : {
156202 createApiUrl : typeof createApiUrl ;
157203 createChecksum : typeof createChecksum ;
204+ createMeetingPassword : typeof createMeetingPassword ;
158205 normalizeServerUrl : typeof normalizeServerUrl ;
159206} = {
160207 createApiUrl,
161208 createChecksum,
209+ createMeetingPassword,
162210 normalizeServerUrl,
163211} ;
164212
0 commit comments