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 22KB

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