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

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