Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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