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

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