You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. /* global $, APP, config, Strophe, Base64, $msg */
  2. /* jshint -W101 */
  3. var Moderator = require("./moderator");
  4. var EventEmitter = require("events");
  5. var Recording = require("./recording");
  6. var SDP = require("./SDP");
  7. var SDPUtil = require("./SDPUtil");
  8. var Settings = require("../settings/Settings");
  9. var Pako = require("pako");
  10. var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
  11. var RTCEvents = require("../../service/RTC/RTCEvents");
  12. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  13. var retry = require('retry');
  14. var RandomUtil = require("../util/RandomUtil");
  15. var eventEmitter = new EventEmitter();
  16. var connection = null;
  17. var authenticatedUser = false;
  18. /**
  19. * Utility method that generates user name based on random hex values.
  20. * Eg. 12345678-1234-1234-12345678
  21. * @returns {string}
  22. */
  23. function generateUserName() {
  24. return RandomUtil.randomHexString(8) + "-" + RandomUtil.randomHexString(4)
  25. + "-" + RandomUtil.randomHexString(4) + "-"
  26. + RandomUtil.randomHexString(8);
  27. }
  28. function connect(jid, password) {
  29. var faultTolerantConnect = retry.operation({
  30. retries: 3
  31. });
  32. // fault tolerant connect
  33. faultTolerantConnect.attempt(function () {
  34. connection = XMPP.createConnection();
  35. Moderator.setConnection(connection);
  36. connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
  37. if (config.useIPv6) {
  38. // https://code.google.com/p/webrtc/issues/detail?id=2828
  39. if (!connection.jingle.pc_constraints.optional)
  40. connection.jingle.pc_constraints.optional = [];
  41. connection.jingle.pc_constraints.optional.push({googIPv6: true});
  42. }
  43. // Include user info in MUC presence
  44. var settings = Settings.getSettings();
  45. if (settings.email) {
  46. connection.emuc.addEmailToPresence(settings.email);
  47. }
  48. if (settings.uid) {
  49. connection.emuc.addUserIdToPresence(settings.uid);
  50. }
  51. if (settings.displayName) {
  52. connection.emuc.addDisplayNameToPresence(settings.displayName);
  53. }
  54. // connection.connect() starts the connection process.
  55. //
  56. // As the connection process proceeds, the user supplied callback will
  57. // be triggered multiple times with status updates. The callback should
  58. // take two arguments - the status code and the error condition.
  59. //
  60. // The status code will be one of the values in the Strophe.Status
  61. // constants. The error condition will be one of the conditions defined
  62. // in RFC 3920 or the condition ‘strophe-parsererror’.
  63. //
  64. // The Parameters wait, hold and route are optional and only relevant
  65. // for BOSH connections. Please see XEP 124 for a more detailed
  66. // explanation of the optional parameters.
  67. //
  68. // Connection status constants for use by the connection handler
  69. // callback.
  70. //
  71. // Status.ERROR - An error has occurred (websockets specific)
  72. // Status.CONNECTING - The connection is currently being made
  73. // Status.CONNFAIL - The connection attempt failed
  74. // Status.AUTHENTICATING - The connection is authenticating
  75. // Status.AUTHFAIL - The authentication attempt failed
  76. // Status.CONNECTED - The connection has succeeded
  77. // Status.DISCONNECTED - The connection has been terminated
  78. // Status.DISCONNECTING - The connection is currently being terminated
  79. // Status.ATTACHED - The connection has been attached
  80. var anonymousConnectionFailed = false;
  81. var connectionFailed = false;
  82. var lastErrorMsg;
  83. connection.connect(jid, password, function (status, msg) {
  84. console.log("(TIME) Strophe " + Strophe.getStatusString(status) +
  85. (msg ? "[" + msg + "]" : "") +
  86. "\t:" + window.performance.now());
  87. if (status === Strophe.Status.CONNECTED) {
  88. if (config.useStunTurn) {
  89. connection.jingle.getStunAndTurnCredentials();
  90. }
  91. console.info("My Jabber ID: " + connection.jid);
  92. // Schedule ping ?
  93. var pingJid = connection.domain;
  94. connection.ping.hasPingSupport(
  95. pingJid,
  96. function (hasPing) {
  97. if (hasPing)
  98. connection.ping.startInterval(pingJid);
  99. else
  100. console.warn("Ping NOT supported by " + pingJid);
  101. }
  102. );
  103. if (password)
  104. authenticatedUser = true;
  105. maybeDoJoin();
  106. } else if (status === Strophe.Status.CONNFAIL) {
  107. if (msg === 'x-strophe-bad-non-anon-jid') {
  108. anonymousConnectionFailed = true;
  109. } else {
  110. connectionFailed = true;
  111. }
  112. lastErrorMsg = msg;
  113. } else if (status === Strophe.Status.DISCONNECTED) {
  114. // Stop ping interval
  115. connection.ping.stopInterval();
  116. if (anonymousConnectionFailed) {
  117. // prompt user for username and password
  118. XMPP.promptLogin();
  119. } else {
  120. // Strophe already has built-in HTTP/BOSH error handling and
  121. // request retry logic. Requests are resent automatically
  122. // until their error count reaches 5. Strophe.js disconnects
  123. // if the error count is > 5. We are not replicating this
  124. // here.
  125. //
  126. // The "problem" is that failed HTTP/BOSH requests don't
  127. // trigger a callback with a status update, so when a
  128. // callback with status Strophe.Status.DISCONNECTED arrives,
  129. // we can't be sure if it's a graceful disconnect or if it's
  130. // triggered by some HTTP/BOSH error.
  131. //
  132. // But that's a minor issue in Jitsi Meet as we never
  133. // disconnect anyway, not even when the user closes the
  134. // browser window (which is kind of wrong, but the point is
  135. // that we should never ever get disconnected).
  136. //
  137. // On the other hand, failed connections due to XMPP layer
  138. // errors, trigger a callback with status Strophe.Status.CONNFAIL.
  139. //
  140. // Here we implement retry logic for failed connections due
  141. // to XMPP layer errors and we display an error to the user
  142. // if we get disconnected from the XMPP server permanently.
  143. // If the connection failed, retry.
  144. if (connectionFailed &&
  145. faultTolerantConnect.retry("connection-failed")) {
  146. return;
  147. }
  148. // If we failed to connect to the XMPP server, fire an event
  149. // to let all the interested module now about it.
  150. eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
  151. msg ? msg : lastErrorMsg);
  152. }
  153. } else if (status === Strophe.Status.AUTHFAIL) {
  154. // wrong password or username, prompt user
  155. XMPP.promptLogin();
  156. }
  157. });
  158. });
  159. }
  160. function maybeDoJoin() {
  161. if (connection && connection.connected &&
  162. Strophe.getResourceFromJid(connection.jid) &&
  163. (APP.RTC.localAudio || APP.RTC.localVideo)) {
  164. // .connected is true while connecting?
  165. doJoin();
  166. }
  167. }
  168. function doJoin() {
  169. eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
  170. }
  171. function initStrophePlugins()
  172. {
  173. require("./strophe.emuc")(XMPP, eventEmitter);
  174. require("./strophe.jingle")(XMPP, eventEmitter);
  175. require("./strophe.moderate")(XMPP, eventEmitter);
  176. require("./strophe.util")();
  177. require("./strophe.ping")(XMPP, eventEmitter);
  178. require("./strophe.rayo")();
  179. require("./strophe.logger")();
  180. }
  181. /**
  182. * If given <tt>localStream</tt> is video one this method will advertise it's
  183. * video type in MUC presence.
  184. * @param localStream new or modified <tt>LocalStream</tt>.
  185. */
  186. function broadcastLocalVideoType(localStream) {
  187. if (localStream.videoType)
  188. XMPP.addToPresence('videoType', localStream.videoType);
  189. }
  190. function registerListeners() {
  191. APP.RTC.addStreamListener(
  192. function (localStream) {
  193. maybeDoJoin();
  194. broadcastLocalVideoType(localStream);
  195. },
  196. StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
  197. );
  198. APP.RTC.addStreamListener(
  199. broadcastLocalVideoType,
  200. StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
  201. );
  202. APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
  203. XMPP.addToPresence("devices", devices);
  204. });
  205. }
  206. var unload = (function () {
  207. var unloaded = false;
  208. return function () {
  209. if (unloaded) { return; }
  210. unloaded = true;
  211. if (connection && connection.connected) {
  212. // ensure signout
  213. $.ajax({
  214. type: 'POST',
  215. url: config.bosh,
  216. async: false,
  217. cache: false,
  218. contentType: 'application/xml',
  219. data: "<body rid='" +
  220. (connection.rid || connection._proto.rid) +
  221. "' xmlns='http://jabber.org/protocol/httpbind' sid='" +
  222. (connection.sid || connection._proto.sid) +
  223. "' type='terminate'>" +
  224. "<presence xmlns='jabber:client' type='unavailable'/>" +
  225. "</body>",
  226. success: function (data) {
  227. console.log('signed out');
  228. console.log(data);
  229. },
  230. error: function (XMLHttpRequest, textStatus, errorThrown) {
  231. console.log('signout error',
  232. textStatus + ' (' + errorThrown + ')');
  233. }
  234. });
  235. }
  236. XMPP.disposeConference(true);
  237. };
  238. })();
  239. function setupEvents() {
  240. // In recent versions of FF the 'beforeunload' event is not fired when the
  241. // window or the tab is closed. It is only fired when we leave the page
  242. // (change URL). If this participant doesn't unload properly, then it
  243. // becomes a ghost for the rest of the participants that stay in the
  244. // conference. Thankfully handling the 'unload' event in addition to the
  245. // 'beforeunload' event seems to guarantee the execution of the 'unload'
  246. // method at least once.
  247. //
  248. // The 'unload' method can safely be run multiple times, it will actually do
  249. // something only the first time that it's run, so we're don't have to worry
  250. // about browsers that fire both events.
  251. $(window).bind('beforeunload', unload);
  252. $(window).bind('unload', unload);
  253. }
  254. var XMPP = {
  255. getConnection: function(){ return connection; },
  256. sessionTerminated: false,
  257. /**
  258. * XMPP connection status
  259. */
  260. Status: Strophe.Status,
  261. /**
  262. * Remembers if we were muted by the focus.
  263. * @type {boolean}
  264. */
  265. forceMuted: false,
  266. start: function () {
  267. setupEvents();
  268. initStrophePlugins();
  269. registerListeners();
  270. Moderator.init(this, eventEmitter);
  271. Recording.init();
  272. var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
  273. // Force authenticated domain if room is appended with '?login=true'
  274. if (config.hosts.anonymousdomain &&
  275. window.location.search.indexOf("login=true") !== -1) {
  276. configDomain = config.hosts.domain;
  277. }
  278. var jid = configDomain || window.location.hostname;
  279. connect(jid);
  280. },
  281. createConnection: function () {
  282. var bosh = config.bosh || '/http-bind';
  283. // adds the room name used to the bosh connection
  284. bosh += '?room=' + APP.UI.getRoomNode();
  285. if (config.token) {
  286. bosh += "&token=" + config.token;
  287. }
  288. return new Strophe.Connection(bosh);
  289. },
  290. getStatusString: function (status) {
  291. return Strophe.getStatusString(status);
  292. },
  293. promptLogin: function () {
  294. eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN, connect);
  295. },
  296. joinRoom: function(roomName, useNicks, nick) {
  297. var roomjid = roomName;
  298. if (useNicks) {
  299. if (nick) {
  300. roomjid += '/' + nick;
  301. } else {
  302. roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
  303. }
  304. } else {
  305. var tmpJid = Strophe.getNodeFromJid(connection.jid);
  306. if(!authenticatedUser)
  307. tmpJid = tmpJid.substr(0, 8);
  308. roomjid += '/' + tmpJid;
  309. }
  310. connection.emuc.doJoin(roomjid);
  311. },
  312. myJid: function () {
  313. if(!connection)
  314. return null;
  315. return connection.emuc.myroomjid;
  316. },
  317. myResource: function () {
  318. if(!connection || ! connection.emuc.myroomjid)
  319. return null;
  320. return Strophe.getResourceFromJid(connection.emuc.myroomjid);
  321. },
  322. getLastPresence: function (from) {
  323. if(!connection)
  324. return null;
  325. return connection.emuc.lastPresenceMap[from];
  326. },
  327. disposeConference: function (onUnload) {
  328. var handler = connection.jingle.activecall;
  329. if (handler && handler.peerconnection) {
  330. // FIXME: probably removing streams is not required and close() should
  331. // be enough
  332. if (APP.RTC.localAudio) {
  333. handler.peerconnection.removeStream(
  334. APP.RTC.localAudio.getOriginalStream(), onUnload);
  335. }
  336. if (APP.RTC.localVideo) {
  337. handler.peerconnection.removeStream(
  338. APP.RTC.localVideo.getOriginalStream(), onUnload);
  339. }
  340. handler.peerconnection.close();
  341. }
  342. eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
  343. connection.jingle.activecall = null;
  344. if (!onUnload) {
  345. this.sessionTerminated = true;
  346. connection.emuc.doLeave();
  347. }
  348. },
  349. addListener: function(type, listener) {
  350. eventEmitter.on(type, listener);
  351. },
  352. removeListener: function (type, listener) {
  353. eventEmitter.removeListener(type, listener);
  354. },
  355. allocateConferenceFocus: function(roomName, callback) {
  356. Moderator.allocateConferenceFocus(roomName, callback);
  357. },
  358. getLoginUrl: function (roomName, callback) {
  359. Moderator.getLoginUrl(roomName, callback);
  360. },
  361. getPopupLoginUrl: function (roomName, callback) {
  362. Moderator.getPopupLoginUrl(roomName, callback);
  363. },
  364. isModerator: function () {
  365. return Moderator.isModerator();
  366. },
  367. isSipGatewayEnabled: function () {
  368. return Moderator.isSipGatewayEnabled();
  369. },
  370. isExternalAuthEnabled: function () {
  371. return Moderator.isExternalAuthEnabled();
  372. },
  373. isConferenceInProgress: function () {
  374. return connection && connection.jingle.activecall &&
  375. connection.jingle.activecall.peerconnection;
  376. },
  377. switchStreams: function (stream, oldStream, callback, isAudio) {
  378. if (this.isConferenceInProgress()) {
  379. // FIXME: will block switchInProgress on true value in case of exception
  380. connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
  381. } else {
  382. // We are done immediately
  383. console.warn("No conference handler or conference not started yet");
  384. callback();
  385. }
  386. },
  387. sendVideoInfoPresence: function (mute) {
  388. if(!connection)
  389. return;
  390. connection.emuc.addVideoInfoToPresence(mute);
  391. connection.emuc.sendPresence();
  392. },
  393. setVideoMute: function (mute, callback, options) {
  394. if(!connection)
  395. return;
  396. var self = this;
  397. var localCallback = function (mute) {
  398. self.sendVideoInfoPresence(mute);
  399. return callback(mute);
  400. };
  401. if(connection.jingle.activecall)
  402. {
  403. connection.jingle.activecall.setVideoMute(
  404. mute, localCallback, options);
  405. }
  406. else {
  407. localCallback(mute);
  408. }
  409. },
  410. setAudioMute: function (mute, callback) {
  411. if (!(connection && APP.RTC.localAudio)) {
  412. return false;
  413. }
  414. if (this.forceMuted && !mute) {
  415. console.info("Asking focus for unmute");
  416. connection.moderate.setMute(connection.emuc.myroomjid, mute);
  417. // FIXME: wait for result before resetting muted status
  418. this.forceMuted = false;
  419. }
  420. if (mute == APP.RTC.localAudio.isMuted()) {
  421. // Nothing to do
  422. return true;
  423. }
  424. APP.RTC.localAudio.setMute(mute);
  425. this.sendAudioInfoPresence(mute, callback);
  426. return true;
  427. },
  428. sendAudioInfoPresence: function(mute, callback) {
  429. if(connection) {
  430. connection.emuc.addAudioInfoToPresence(mute);
  431. connection.emuc.sendPresence();
  432. }
  433. callback();
  434. return true;
  435. },
  436. toggleRecording: function (tokenEmptyCallback,
  437. recordingStateChangeCallback) {
  438. Recording.toggleRecording(tokenEmptyCallback,
  439. recordingStateChangeCallback, connection);
  440. },
  441. addToPresence: function (name, value, dontSend) {
  442. switch (name) {
  443. case "displayName":
  444. connection.emuc.addDisplayNameToPresence(value);
  445. break;
  446. case "prezi":
  447. connection.emuc.addPreziToPresence(value, 0);
  448. break;
  449. case "preziSlide":
  450. connection.emuc.addCurrentSlideToPresence(value);
  451. break;
  452. case "connectionQuality":
  453. connection.emuc.addConnectionInfoToPresence(value);
  454. break;
  455. case "email":
  456. connection.emuc.addEmailToPresence(value);
  457. break;
  458. case "devices":
  459. connection.emuc.addDevicesToPresence(value);
  460. break;
  461. case "videoType":
  462. connection.emuc.addVideoTypeToPresence(value);
  463. break;
  464. case "startMuted":
  465. if(!Moderator.isModerator())
  466. return;
  467. connection.emuc.addStartMutedToPresence(value[0],
  468. value[1]);
  469. break;
  470. default :
  471. console.log("Unknown tag for presence: " + name);
  472. return;
  473. }
  474. if (!dontSend)
  475. connection.emuc.sendPresence();
  476. },
  477. /**
  478. * Sends 'data' as a log message to the focus. Returns true iff a message
  479. * was sent.
  480. * @param data
  481. * @returns {boolean} true iff a message was sent.
  482. */
  483. sendLogs: function (data) {
  484. if(!connection.emuc.focusMucJid)
  485. return false;
  486. var deflate = true;
  487. var content = JSON.stringify(data);
  488. if (deflate) {
  489. content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
  490. }
  491. content = Base64.encode(content);
  492. // XEP-0337-ish
  493. var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
  494. message.c('log', { xmlns: 'urn:xmpp:eventlog',
  495. id: 'PeerConnectionStats'});
  496. message.c('message').t(content).up();
  497. if (deflate) {
  498. message.c('tag', {name: "deflated", value: "true"}).up();
  499. }
  500. message.up();
  501. connection.send(message);
  502. return true;
  503. },
  504. // Gets the logs from strophe.jingle.
  505. getJingleLog: function () {
  506. return connection.jingle ? connection.jingle.getLog() : {};
  507. },
  508. // Gets the logs from strophe.
  509. getXmppLog: function () {
  510. return connection.logger ? connection.logger.log : null;
  511. },
  512. getPrezi: function () {
  513. return connection.emuc.getPrezi(this.myJid());
  514. },
  515. removePreziFromPresence: function () {
  516. connection.emuc.removePreziFromPresence();
  517. connection.emuc.sendPresence();
  518. },
  519. sendChatMessage: function (message, nickname) {
  520. connection.emuc.sendMessage(message, nickname);
  521. },
  522. setSubject: function (topic) {
  523. connection.emuc.setSubject(topic);
  524. },
  525. lockRoom: function (key, onSuccess, onError, onNotSupported) {
  526. connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
  527. },
  528. dial: function (to, from, roomName,roomPass) {
  529. connection.rayo.dial(to, from, roomName,roomPass);
  530. },
  531. setMute: function (jid, mute) {
  532. connection.moderate.setMute(jid, mute);
  533. },
  534. eject: function (jid) {
  535. connection.moderate.eject(jid);
  536. },
  537. logout: function (callback) {
  538. Moderator.logout(callback);
  539. },
  540. findJidFromResource: function (resource) {
  541. return connection.emuc.findJidFromResource(resource);
  542. },
  543. getMembers: function () {
  544. return connection.emuc.members;
  545. },
  546. getJidFromSSRC: function (ssrc) {
  547. if (!this.isConferenceInProgress())
  548. return null;
  549. return connection.jingle.activecall.getSsrcOwner(ssrc);
  550. },
  551. /**
  552. * Gets the SSRC of local media stream.
  553. * @param mediaType the media type that tells whether we want to get
  554. * the SSRC of local audio or video stream.
  555. * @returns {*} the SSRC number for local media stream or <tt>null</tt> if
  556. * not available.
  557. */
  558. getLocalSSRC: function (mediaType) {
  559. if (!this.isConferenceInProgress()) {
  560. return null;
  561. }
  562. return connection.jingle.activecall.getLocalSSRC(mediaType);
  563. },
  564. // Returns true iff we have joined the MUC.
  565. isMUCJoined: function () {
  566. return connection === null ? false : connection.emuc.joined;
  567. },
  568. getSessions: function () {
  569. return connection.jingle.sessions;
  570. },
  571. removeStream: function (stream) {
  572. if (!this.isConferenceInProgress())
  573. return;
  574. connection.jingle.activecall.peerconnection.removeStream(stream);
  575. },
  576. filter_special_chars: function (text) {
  577. return SDPUtil.filter_special_chars(text);
  578. }
  579. };
  580. module.exports = XMPP;