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.

xmpp.js 21KB

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