Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

moderator.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. /* global $, $iq, Promise, Strophe */
  2. const logger = require('jitsi-meet-logger').getLogger(__filename);
  3. const XMPPEvents = require('../../service/xmpp/XMPPEvents');
  4. const AuthenticationEvents
  5. = require('../../service/authentication/AuthenticationEvents');
  6. const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
  7. import Settings from '../settings/Settings';
  8. /**
  9. *
  10. * @param step
  11. */
  12. function createExpBackoffTimer(step) {
  13. let count = 1;
  14. return function(reset) {
  15. // Reset call
  16. if (reset) {
  17. count = 1;
  18. return;
  19. }
  20. // Calculate next timeout
  21. const timeout = Math.pow(2, count - 1);
  22. count += 1;
  23. return timeout * step;
  24. };
  25. }
  26. /* eslint-disable max-params */
  27. /**
  28. *
  29. * @param roomName
  30. * @param xmpp
  31. * @param emitter
  32. * @param options
  33. */
  34. export default function Moderator(roomName, xmpp, emitter, options) {
  35. this.roomName = roomName;
  36. this.xmppService = xmpp;
  37. this.getNextTimeout = createExpBackoffTimer(1000);
  38. this.getNextErrorTimeout = createExpBackoffTimer(1000);
  39. // External authentication stuff
  40. this.externalAuthEnabled = false;
  41. this.options = options;
  42. // Sip gateway can be enabled by configuring Jigasi host in config.js or
  43. // it will be enabled automatically if focus detects the component through
  44. // service discovery.
  45. this.sipGatewayEnabled
  46. = this.options.connection.hosts
  47. && this.options.connection.hosts.call_control !== undefined;
  48. this.eventEmitter = emitter;
  49. this.connection = this.xmppService.connection;
  50. // FIXME: Message listener that talks to POPUP window
  51. /**
  52. *
  53. * @param event
  54. */
  55. function listener(event) {
  56. if (event.data && event.data.sessionId) {
  57. if (event.origin !== window.location.origin) {
  58. logger.warn(
  59. `Ignoring sessionId from different origin: ${
  60. event.origin}`);
  61. return;
  62. }
  63. Settings.setSessionId(event.data.sessionId);
  64. // After popup is closed we will authenticate
  65. }
  66. }
  67. // Register
  68. if (window.addEventListener) {
  69. window.addEventListener('message', listener, false);
  70. } else {
  71. window.attachEvent('onmessage', listener);
  72. }
  73. }
  74. /* eslint-enable max-params */
  75. Moderator.prototype.isExternalAuthEnabled = function() {
  76. return this.externalAuthEnabled;
  77. };
  78. Moderator.prototype.isSipGatewayEnabled = function() {
  79. return this.sipGatewayEnabled;
  80. };
  81. Moderator.prototype.onMucMemberLeft = function(jid) {
  82. logger.info(`Someone left is it focus ? ${jid}`);
  83. const resource = Strophe.getResourceFromJid(jid);
  84. if (resource === 'focus') {
  85. logger.info(
  86. 'Focus has left the room - leaving conference');
  87. this.eventEmitter.emit(XMPPEvents.FOCUS_LEFT);
  88. }
  89. };
  90. Moderator.prototype.setFocusUserJid = function(focusJid) {
  91. if (!this.focusUserJid) {
  92. this.focusUserJid = focusJid;
  93. logger.info(`Focus jid set to: ${this.focusUserJid}`);
  94. }
  95. };
  96. Moderator.prototype.getFocusUserJid = function() {
  97. return this.focusUserJid;
  98. };
  99. Moderator.prototype.getFocusComponent = function() {
  100. // Get focus component address
  101. let focusComponent = this.options.connection.hosts.focus;
  102. // If not specified use default: 'focus.domain'
  103. if (!focusComponent) {
  104. focusComponent = `focus.${this.options.connection.hosts.domain}`;
  105. }
  106. return focusComponent;
  107. };
  108. Moderator.prototype.createConferenceIq = function() {
  109. // Generate create conference IQ
  110. const elem = $iq({ to: this.getFocusComponent(),
  111. type: 'set' });
  112. // Session Id used for authentication
  113. const sessionId = Settings.getSessionId();
  114. const machineUID = Settings.getMachineId();
  115. logger.info(`Session ID: ${sessionId} machine UID: ${machineUID}`);
  116. elem.c('conference', {
  117. xmlns: 'http://jitsi.org/protocol/focus',
  118. room: this.roomName,
  119. 'machine-uid': machineUID
  120. });
  121. if (sessionId) {
  122. elem.attrs({ 'session-id': sessionId });
  123. }
  124. if (this.options.connection.enforcedBridge !== undefined) {
  125. elem.c(
  126. 'property', {
  127. name: 'enforcedBridge',
  128. value: this.options.connection.enforcedBridge
  129. }).up();
  130. }
  131. // Tell the focus we have Jigasi configured
  132. if (this.options.connection.hosts !== undefined
  133. && this.options.connection.hosts.call_control !== undefined) {
  134. elem.c(
  135. 'property', {
  136. name: 'call_control',
  137. value: this.options.connection.hosts.call_control
  138. }).up();
  139. }
  140. if (this.options.conference.channelLastN !== undefined) {
  141. elem.c(
  142. 'property', {
  143. name: 'channelLastN',
  144. value: this.options.conference.channelLastN
  145. }).up();
  146. }
  147. elem.c(
  148. 'property', {
  149. name: 'disableRtx',
  150. value: Boolean(this.options.conference.disableRtx)
  151. }).up();
  152. elem.c(
  153. 'property', {
  154. name: 'enableLipSync',
  155. value: this.options.connection.enableLipSync !== false
  156. }).up();
  157. if (this.options.conference.audioPacketDelay !== undefined) {
  158. elem.c(
  159. 'property', {
  160. name: 'audioPacketDelay',
  161. value: this.options.conference.audioPacketDelay
  162. }).up();
  163. }
  164. if (this.options.conference.startBitrate) {
  165. elem.c(
  166. 'property', {
  167. name: 'startBitrate',
  168. value: this.options.conference.startBitrate
  169. }).up();
  170. }
  171. if (this.options.conference.minBitrate) {
  172. elem.c(
  173. 'property', {
  174. name: 'minBitrate',
  175. value: this.options.conference.minBitrate
  176. }).up();
  177. }
  178. if (this.options.conference.openSctp !== undefined) {
  179. elem.c(
  180. 'property', {
  181. name: 'openSctp',
  182. value: this.options.conference.openSctp
  183. }).up();
  184. }
  185. if (this.options.conference.startAudioMuted !== undefined) {
  186. elem.c(
  187. 'property', {
  188. name: 'startAudioMuted',
  189. value: this.options.conference.startAudioMuted
  190. }).up();
  191. }
  192. if (this.options.conference.startVideoMuted !== undefined) {
  193. elem.c(
  194. 'property', {
  195. name: 'startVideoMuted',
  196. value: this.options.conference.startVideoMuted
  197. }).up();
  198. }
  199. if (this.options.conference.stereo !== undefined) {
  200. elem.c(
  201. 'property', {
  202. name: 'stereo',
  203. value: this.options.conference.stereo
  204. }).up();
  205. }
  206. if (this.options.conference.useRoomAsSharedDocumentName !== undefined) {
  207. elem.c(
  208. 'property', {
  209. name: 'useRoomAsSharedDocumentName',
  210. value: this.options.conference.useRoomAsSharedDocumentName
  211. }).up();
  212. }
  213. elem.up();
  214. return elem;
  215. };
  216. Moderator.prototype.parseSessionId = function(resultIq) {
  217. // eslint-disable-next-line newline-per-chained-call
  218. const sessionId = $(resultIq).find('conference').attr('session-id');
  219. if (sessionId) {
  220. logger.info(`Received sessionId: ${sessionId}`);
  221. Settings.setSessionId(sessionId);
  222. }
  223. };
  224. Moderator.prototype.parseConfigOptions = function(resultIq) {
  225. // eslint-disable-next-line newline-per-chained-call
  226. this.setFocusUserJid($(resultIq).find('conference').attr('focusjid'));
  227. const authenticationEnabled
  228. = $(resultIq).find(
  229. '>conference>property'
  230. + '[name=\'authentication\'][value=\'true\']').length > 0;
  231. logger.info(`Authentication enabled: ${authenticationEnabled}`);
  232. this.externalAuthEnabled = $(resultIq).find(
  233. '>conference>property'
  234. + '[name=\'externalAuth\'][value=\'true\']').length > 0;
  235. logger.info(
  236. `External authentication enabled: ${this.externalAuthEnabled}`);
  237. if (!this.externalAuthEnabled) {
  238. // We expect to receive sessionId in 'internal' authentication mode
  239. this.parseSessionId(resultIq);
  240. }
  241. // eslint-disable-next-line newline-per-chained-call
  242. const authIdentity = $(resultIq).find('>conference').attr('identity');
  243. this.eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
  244. authenticationEnabled, authIdentity);
  245. // Check if focus has auto-detected Jigasi component(this will be also
  246. // included if we have passed our host from the config)
  247. if ($(resultIq).find(
  248. '>conference>property'
  249. + '[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
  250. this.sipGatewayEnabled = true;
  251. }
  252. logger.info(`Sip gateway enabled: ${this.sipGatewayEnabled}`);
  253. };
  254. // FIXME We need to show the fact that we're waiting for the focus to the user
  255. // (or that the focus is not available)
  256. /**
  257. * Allocates the conference focus.
  258. *
  259. * @param {Function} callback - the function to be called back upon the
  260. * successful allocation of the conference focus
  261. */
  262. Moderator.prototype.allocateConferenceFocus = function(callback) {
  263. // Try to use focus user JID from the config
  264. this.setFocusUserJid(this.options.connection.focusUserJid);
  265. // Send create conference IQ
  266. this.connection.sendIQ(
  267. this.createConferenceIq(),
  268. result => this._allocateConferenceFocusSuccess(result, callback),
  269. error => this._allocateConferenceFocusError(error, callback));
  270. // XXX We're pressed for time here because we're beginning a complex and/or
  271. // lengthy conference-establishment process which supposedly involves
  272. // multiple RTTs. We don't have the time to wait for Strophe to decide to
  273. // send our IQ.
  274. this.connection.flush();
  275. };
  276. /**
  277. * Invoked by {@link #allocateConferenceFocus} upon its request receiving an
  278. * error result.
  279. *
  280. * @param error - the error result of the request that
  281. * {@link #allocateConferenceFocus} sent
  282. * @param {Function} callback - the function to be called back upon the
  283. * successful allocation of the conference focus
  284. */
  285. Moderator.prototype._allocateConferenceFocusError = function(error, callback) {
  286. // If the session is invalid, remove and try again without session ID to get
  287. // a new one
  288. const invalidSession = $(error).find('>error>session-invalid').length;
  289. if (invalidSession) {
  290. logger.info('Session expired! - removing');
  291. Settings.clearSessionId();
  292. }
  293. if ($(error).find('>error>graceful-shutdown').length) {
  294. this.eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
  295. return;
  296. }
  297. // Check for error returned by the reservation system
  298. const reservationErr = $(error).find('>error>reservation-error');
  299. if (reservationErr.length) {
  300. // Trigger error event
  301. const errorCode = reservationErr.attr('error-code');
  302. const errorTextNode = $(error).find('>error>text');
  303. let errorMsg;
  304. if (errorTextNode) {
  305. errorMsg = errorTextNode.text();
  306. }
  307. this.eventEmitter.emit(
  308. XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
  309. return;
  310. }
  311. // Not authorized to create new room
  312. if ($(error).find('>error>not-authorized').length) {
  313. logger.warn('Unauthorized to start the conference', error);
  314. const toDomain = Strophe.getDomainFromJid(error.getAttribute('to'));
  315. if (toDomain !== this.options.connection.hosts.anonymousdomain) {
  316. // FIXME "is external" should come either from the focus or
  317. // config.js
  318. this.externalAuthEnabled = true;
  319. }
  320. this.eventEmitter.emit(XMPPEvents.AUTHENTICATION_REQUIRED);
  321. return;
  322. }
  323. const waitMs = this.getNextErrorTimeout();
  324. const errmsg = `Focus error, retry after ${waitMs}`;
  325. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  326. logger.error(errmsg, error);
  327. // Show message
  328. const focusComponent = this.getFocusComponent();
  329. const retrySec = waitMs / 1000;
  330. // FIXME: message is duplicated ? Do not show in case of session invalid
  331. // which means just a retry
  332. if (!invalidSession) {
  333. this.eventEmitter.emit(
  334. XMPPEvents.FOCUS_DISCONNECTED, focusComponent, retrySec);
  335. }
  336. // Reset response timeout
  337. this.getNextTimeout(true);
  338. window.setTimeout(() => this.allocateConferenceFocus(callback), waitMs);
  339. };
  340. /**
  341. * Invoked by {@link #allocateConferenceFocus} upon its request receiving a
  342. * success (i.e. non-error) result.
  343. *
  344. * @param result - the success (i.e. non-error) result of the request that
  345. * {@link #allocateConferenceFocus} sent
  346. * @param {Function} callback - the function to be called back upon the
  347. * successful allocation of the conference focus
  348. */
  349. Moderator.prototype._allocateConferenceFocusSuccess = function(
  350. result,
  351. callback) {
  352. // Setup config options
  353. this.parseConfigOptions(result);
  354. // Reset the error timeout (because we haven't failed here).
  355. this.getNextErrorTimeout(true);
  356. // eslint-disable-next-line newline-per-chained-call
  357. if ($(result).find('conference').attr('ready') === 'true') {
  358. // Reset the non-error timeout (because we've succeeded here).
  359. this.getNextTimeout(true);
  360. // Exec callback
  361. callback();
  362. } else {
  363. const waitMs = this.getNextTimeout();
  364. logger.info(`Waiting for the focus... ${waitMs}`);
  365. window.setTimeout(() => this.allocateConferenceFocus(callback),
  366. waitMs);
  367. }
  368. };
  369. Moderator.prototype.authenticate = function() {
  370. return new Promise((resolve, reject) => {
  371. this.connection.sendIQ(
  372. this.createConferenceIq(),
  373. result => {
  374. this.parseSessionId(result);
  375. resolve();
  376. }, error => {
  377. // eslint-disable-next-line newline-per-chained-call
  378. const code = $(error).find('>error').attr('code');
  379. reject(error, code);
  380. }
  381. );
  382. });
  383. };
  384. Moderator.prototype.getLoginUrl = function(urlCallback, failureCallback) {
  385. this._getLoginUrl(/* popup */ false, urlCallback, failureCallback);
  386. };
  387. /**
  388. *
  389. * @param {boolean} popup false for {@link Moderator#getLoginUrl} or true for
  390. * {@link Moderator#getPopupLoginUrl}
  391. * @param urlCb
  392. * @param failureCb
  393. */
  394. Moderator.prototype._getLoginUrl = function(popup, urlCb, failureCb) {
  395. const iq = $iq({ to: this.getFocusComponent(),
  396. type: 'get' });
  397. const attrs = {
  398. xmlns: 'http://jitsi.org/protocol/focus',
  399. room: this.roomName,
  400. 'machine-uid': Settings.getMachineId()
  401. };
  402. let str = 'auth url'; // for logger
  403. if (popup) {
  404. attrs.popup = true;
  405. str = `POPUP ${str}`;
  406. }
  407. iq.c('login-url', attrs);
  408. /**
  409. * Implements a failure callback which reports an error message and an error
  410. * through (1) GlobalOnErrorHandler, (2) logger, and (3) failureCb.
  411. *
  412. * @param {string} errmsg the error messsage to report
  413. * @param {*} error the error to report (in addition to errmsg)
  414. */
  415. function reportError(errmsg, err) {
  416. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  417. logger.error(errmsg, err);
  418. failureCb(err);
  419. }
  420. this.connection.sendIQ(
  421. iq,
  422. result => {
  423. // eslint-disable-next-line newline-per-chained-call
  424. let url = $(result).find('login-url').attr('url');
  425. url = decodeURIComponent(url);
  426. if (url) {
  427. logger.info(`Got ${str}: ${url}`);
  428. urlCb(url);
  429. } else {
  430. reportError(`Failed to get ${str} from the focus`, result);
  431. }
  432. },
  433. reportError.bind(undefined, `Get ${str} error`)
  434. );
  435. };
  436. Moderator.prototype.getPopupLoginUrl = function(urlCallback, failureCallback) {
  437. this._getLoginUrl(/* popup */ true, urlCallback, failureCallback);
  438. };
  439. Moderator.prototype.logout = function(callback) {
  440. const iq = $iq({ to: this.getFocusComponent(),
  441. type: 'set' });
  442. const sessionId = Settings.getSessionId();
  443. if (!sessionId) {
  444. callback();
  445. return;
  446. }
  447. iq.c('logout', {
  448. xmlns: 'http://jitsi.org/protocol/focus',
  449. 'session-id': sessionId
  450. });
  451. this.connection.sendIQ(
  452. iq,
  453. result => {
  454. // eslint-disable-next-line newline-per-chained-call
  455. let logoutUrl = $(result).find('logout').attr('logout-url');
  456. if (logoutUrl) {
  457. logoutUrl = decodeURIComponent(logoutUrl);
  458. }
  459. logger.info(`Log out OK, url: ${logoutUrl}`, result);
  460. Settings.clearSessionId();
  461. callback(logoutUrl);
  462. },
  463. error => {
  464. const errmsg = 'Logout error';
  465. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  466. logger.error(errmsg, error);
  467. }
  468. );
  469. };