Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

xmpp.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /* global $, APP, config, Strophe */
  2. var logger = require("jitsi-meet-logger").getLogger(__filename);
  3. var EventEmitter = require("events");
  4. var Pako = require("pako");
  5. var RTCEvents = require("../../service/RTC/RTCEvents");
  6. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  7. var JitsiConnectionErrors = require("../../JitsiConnectionErrors");
  8. var JitsiConnectionEvents = require("../../JitsiConnectionEvents");
  9. var RTC = require("../RTC/RTC");
  10. var RTCBrowserType = require("../RTC/RTCBrowserType");
  11. var authenticatedUser = false;
  12. function createConnection(bosh, token) {
  13. bosh = bosh || '/http-bind';
  14. // Append token as URL param
  15. if (token) {
  16. bosh += (bosh.indexOf('?') == -1 ? '?' : '&') + 'token=' + token;
  17. }
  18. return new Strophe.Connection(bosh);
  19. };
  20. //!!!!!!!!!! FIXME: ...
  21. function initStrophePlugins(XMPP) {
  22. require("./strophe.emuc")(XMPP);
  23. require("./strophe.jingle")(XMPP, XMPP.eventEmitter);
  24. // require("./strophe.moderate")(XMPP, eventEmitter);
  25. require("./strophe.util")();
  26. require("./strophe.ping")(XMPP, XMPP.eventEmitter);
  27. require("./strophe.rayo")();
  28. require("./strophe.logger")();
  29. }
  30. function XMPP(options, token) {
  31. this.eventEmitter = new EventEmitter();
  32. this.connection = null;
  33. this.disconnectInProgress = false;
  34. this.connectionTimes = {};
  35. this.forceMuted = false;
  36. this.options = options;
  37. this.connectParams = {};
  38. this.token = token;
  39. initStrophePlugins(this);
  40. this.connection = createConnection(options.bosh, token);
  41. // Initialize features advertised in disco-info
  42. this.initFeaturesList();
  43. // Setup a disconnect on unload as a way to facilitate API consumers. It
  44. // sounds like they would want that. A problem for them though may be if
  45. // they wanted to utilize the connected connection in an unload handler of
  46. // their own. However, it should be fairly easy for them to do that by
  47. // registering their unload handler before us.
  48. $(window).on('beforeunload unload', this.disconnect.bind(this));
  49. }
  50. /**
  51. * Reloads the XMPP module
  52. * @param options {object} options to be overriden
  53. */
  54. XMPP.prototype.reload = function (options) {
  55. if(!options)
  56. options = {};
  57. if(!this.options) {
  58. this.options = options;
  59. } else {
  60. // Override config options
  61. for(var key in options)
  62. this.options[key] = options[key] || this.options[key];
  63. }
  64. this.disconnect();
  65. this.connection.pause();
  66. this.connection = createConnection(this.options.bosh, this.token);
  67. // Initialize features advertised in disco-info
  68. this.initFeaturesList();
  69. //getData for attach
  70. if(this.options.prebindURL &&
  71. typeof(createConnectionExternally) === "function") {
  72. var self = this;
  73. createConnectionExternally(this.options.prebindURL, function (data) {
  74. self.attach(data);
  75. }, function (error) {
  76. //connect
  77. self.connect(this.connectParams.jid, this.connectParams.password);
  78. });
  79. } else {
  80. //connect
  81. this.connect(this.connectParams.jid, this.connectParams.password);
  82. }
  83. }
  84. /**
  85. * Initializes the list of feature advertised through the disco-info mechanism
  86. */
  87. XMPP.prototype.initFeaturesList = function () {
  88. var disco = this.connection.disco;
  89. if (disco) {
  90. // http://xmpp.org/extensions/xep-0167.html#support
  91. // http://xmpp.org/extensions/xep-0176.html#support
  92. disco.addFeature('urn:xmpp:jingle:1');
  93. disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
  94. disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
  95. disco.addFeature('urn:xmpp:jingle:apps:dtls:0');
  96. disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
  97. disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
  98. disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
  99. if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()
  100. || RTCBrowserType.isTemasysPluginUsed()) {
  101. disco.addFeature('urn:ietf:rfc:4588');
  102. }
  103. // this is dealt with by SDP O/A so we don't need to announce this
  104. //disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
  105. //disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
  106. disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
  107. disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
  108. //disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
  109. // Enable Lipsync ?
  110. if (this.options.enableLipSync && RTCBrowserType.isChrome()) {
  111. logger.info("Lip-sync enabled !");
  112. this.connection.disco.addFeature('http://jitsi.org/meet/lipsync');
  113. }
  114. }
  115. };
  116. XMPP.prototype.getConnection = function () { return this.connection; };
  117. /**
  118. * Receive connection status changes and handles them.
  119. * @password {string} the password passed in connect method
  120. * @status the connection status
  121. * @msg message
  122. */
  123. XMPP.prototype.connectionHandler = function (password, status, msg) {
  124. var now = window.performance.now();
  125. this.connectionTimes[Strophe.getStatusString(status).toLowerCase()] = now;
  126. logger.log("(TIME) Strophe " + Strophe.getStatusString(status) +
  127. (msg ? "[" + msg + "]" : "") + ":\t", now);
  128. if (status === Strophe.Status.CONNECTED ||
  129. status === Strophe.Status.ATTACHED) {
  130. if (this.options.useStunTurn) {
  131. this.connection.jingle.getStunAndTurnCredentials();
  132. }
  133. logger.info("My Jabber ID: " + this.connection.jid);
  134. // Schedule ping ?
  135. var pingJid = this.connection.domain;
  136. this.connection.ping.hasPingSupport(
  137. pingJid,
  138. function (hasPing) {
  139. if (hasPing)
  140. this.connection.ping.startInterval(pingJid);
  141. else
  142. logger.warn("Ping NOT supported by " + pingJid);
  143. }.bind(this));
  144. if (password)
  145. authenticatedUser = true;
  146. if (this.connection && this.connection.connected &&
  147. Strophe.getResourceFromJid(this.connection.jid)) {
  148. // .connected is true while connecting?
  149. // this.connection.send($pres());
  150. this.eventEmitter.emit(
  151. JitsiConnectionEvents.CONNECTION_ESTABLISHED,
  152. Strophe.getResourceFromJid(this.connection.jid));
  153. }
  154. } else if (status === Strophe.Status.CONNFAIL) {
  155. if (msg === 'x-strophe-bad-non-anon-jid') {
  156. this.anonymousConnectionFailed = true;
  157. } else {
  158. this.connectionFailed = true;
  159. }
  160. this.lastErrorMsg = msg;
  161. } else if (status === Strophe.Status.DISCONNECTED) {
  162. // Stop ping interval
  163. this.connection.ping.stopInterval();
  164. this.disconnectInProgress = false;
  165. if (this.anonymousConnectionFailed) {
  166. // prompt user for username and password
  167. this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
  168. JitsiConnectionErrors.PASSWORD_REQUIRED);
  169. } else if(this.connectionFailed) {
  170. this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
  171. JitsiConnectionErrors.OTHER_ERROR,
  172. msg ? msg : this.lastErrorMsg);
  173. } else {
  174. this.eventEmitter.emit(
  175. JitsiConnectionEvents.CONNECTION_DISCONNECTED,
  176. msg ? msg : this.lastErrorMsg);
  177. }
  178. } else if (status === Strophe.Status.AUTHFAIL) {
  179. // wrong password or username, prompt user
  180. this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
  181. JitsiConnectionErrors.PASSWORD_REQUIRED);
  182. }
  183. }
  184. XMPP.prototype._connect = function (jid, password) {
  185. // connection.connect() starts the connection process.
  186. //
  187. // As the connection process proceeds, the user supplied callback will
  188. // be triggered multiple times with status updates. The callback should
  189. // take two arguments - the status code and the error condition.
  190. //
  191. // The status code will be one of the values in the Strophe.Status
  192. // constants. The error condition will be one of the conditions defined
  193. // in RFC 3920 or the condition ‘strophe-parsererror’.
  194. //
  195. // The Parameters wait, hold and route are optional and only relevant
  196. // for BOSH connections. Please see XEP 124 for a more detailed
  197. // explanation of the optional parameters.
  198. //
  199. // Connection status constants for use by the connection handler
  200. // callback.
  201. //
  202. // Status.ERROR - An error has occurred (websockets specific)
  203. // Status.CONNECTING - The connection is currently being made
  204. // Status.CONNFAIL - The connection attempt failed
  205. // Status.AUTHENTICATING - The connection is authenticating
  206. // Status.AUTHFAIL - The authentication attempt failed
  207. // Status.CONNECTED - The connection has succeeded
  208. // Status.DISCONNECTED - The connection has been terminated
  209. // Status.DISCONNECTING - The connection is currently being terminated
  210. // Status.ATTACHED - The connection has been attached
  211. this.anonymousConnectionFailed = false;
  212. this.connectionFailed = false;
  213. this.lastErrorMsg = undefined;
  214. this.connection.connect(jid, password,
  215. this.connectionHandler.bind(this, password));
  216. }
  217. /**
  218. * Attach to existing connection. Can be used for optimizations. For example:
  219. * if the connection is created on the server we can attach to it and start
  220. * using it.
  221. *
  222. * @param options {object} connecting options - rid, sid, jid and password.
  223. */
  224. XMPP.prototype.attach = function (options) {
  225. var now = this.connectionTimes["attaching"] = window.performance.now();
  226. logger.log("(TIME) Strophe Attaching\t:" + now);
  227. this.connection.attach(options.jid, options.sid, parseInt(options.rid,10)+1,
  228. this.connectionHandler.bind(this, options.password));
  229. }
  230. XMPP.prototype.connect = function (jid, password) {
  231. this.connectParams = {
  232. jid: jid,
  233. password: password
  234. };
  235. if (!jid) {
  236. var configDomain
  237. = this.options.hosts.anonymousdomain || this.options.hosts.domain;
  238. // Force authenticated domain if room is appended with '?login=true'
  239. if (this.options.hosts.anonymousdomain
  240. && window.location.search.indexOf("login=true") !== -1) {
  241. configDomain = this.options.hosts.domain;
  242. }
  243. jid = configDomain || window.location.hostname;
  244. }
  245. return this._connect(jid, password);
  246. };
  247. XMPP.prototype.createRoom = function (roomName, options, settings, maxRetries) {
  248. var roomjid = roomName + '@' + this.options.hosts.muc;
  249. if (options.useNicks) {
  250. if (options.nick) {
  251. roomjid += '/' + options.nick;
  252. } else {
  253. roomjid += '/' + Strophe.getNodeFromJid(this.connection.jid);
  254. }
  255. } else {
  256. var tmpJid = Strophe.getNodeFromJid(this.connection.jid);
  257. if (!authenticatedUser)
  258. tmpJid = tmpJid.substr(0, 8);
  259. roomjid += '/' + tmpJid;
  260. }
  261. return this.connection.emuc.createRoom(roomjid, null, options, settings,
  262. maxRetries);
  263. }
  264. XMPP.prototype.addListener = function(type, listener) {
  265. this.eventEmitter.on(type, listener);
  266. };
  267. XMPP.prototype.removeListener = function (type, listener) {
  268. this.eventEmitter.removeListener(type, listener);
  269. };
  270. /**
  271. * Sends 'data' as a log message to the focus. Returns true iff a message
  272. * was sent.
  273. * @param data
  274. * @returns {boolean} true iff a message was sent.
  275. */
  276. XMPP.prototype.sendLogs = function (data) {
  277. if (!this.connection.emuc.focusMucJid)
  278. return false;
  279. var deflate = true;
  280. var content = JSON.stringify(data);
  281. if (deflate) {
  282. content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
  283. }
  284. content = Base64.encode(content);
  285. // XEP-0337-ish
  286. var message = $msg({to: this.connection.emuc.focusMucJid, type: 'normal'});
  287. message.c('log', {xmlns: 'urn:xmpp:eventlog', id: 'PeerConnectionStats'});
  288. message.c('message').t(content).up();
  289. if (deflate) {
  290. message.c('tag', {name: "deflated", value: "true"}).up();
  291. }
  292. message.up();
  293. this.connection.send(message);
  294. return true;
  295. };
  296. // Gets the logs from strophe.jingle.
  297. XMPP.prototype.getJingleLog = function () {
  298. return this.connection.jingle ? this.connection.jingle.getLog() : {};
  299. };
  300. // Gets the logs from strophe.
  301. XMPP.prototype.getXmppLog = function () {
  302. return this.connection.logger ? this.connection.logger.log : null;
  303. };
  304. XMPP.prototype.dial = function (to, from, roomName,roomPass) {
  305. this.connection.rayo.dial(to, from, roomName,roomPass);
  306. };
  307. XMPP.prototype.setMute = function (jid, mute) {
  308. this.connection.moderate.setMute(jid, mute);
  309. };
  310. XMPP.prototype.eject = function (jid) {
  311. this.connection.moderate.eject(jid);
  312. };
  313. XMPP.prototype.getSessions = function () {
  314. return this.connection.jingle.sessions;
  315. };
  316. /**
  317. * Disconnects this from the XMPP server (if this is connected).
  318. *
  319. * @param ev optionally, the event which triggered the necessity to disconnect
  320. * from the XMPP server (e.g. beforeunload, unload)
  321. */
  322. XMPP.prototype.disconnect = function (ev) {
  323. if (this.disconnectInProgress
  324. || !this.connection
  325. || !this.connection.connected) {
  326. this.eventEmitter.emit(JitsiConnectionEvents.WRONG_STATE);
  327. return;
  328. }
  329. this.disconnectInProgress = true;
  330. // XXX Strophe is asynchronously sending by default. Unfortunately, that
  331. // means that there may not be enough time to send an unavailable presence
  332. // or disconnect at all. Switching Strophe to synchronous sending is not
  333. // much of an option because it may lead to a noticeable delay in navigating
  334. // away from the current location. As a compromise, we will try to increase
  335. // the chances of sending an unavailable presence and/or disconecting within
  336. // the short time span that we have upon unloading by invoking flush() on
  337. // the connection. We flush() once before disconnect() in order to attemtp
  338. // to have its unavailable presence at the top of the send queue. We flush()
  339. // once more after disconnect() in order to attempt to have its unavailable
  340. // presence sent as soon as possible.
  341. this.connection.flush();
  342. if (ev !== null && typeof ev !== 'undefined') {
  343. var evType = ev.type;
  344. if (evType == 'beforeunload' || evType == 'unload') {
  345. // XXX Whatever we said above, synchronous sending is the best
  346. // (known) way to properly disconnect from the XMPP server.
  347. // Consequently, it may be fine to have the source code and comment
  348. // it in or out depending on whether we want to run with it for some
  349. // time.
  350. this.connection.options.sync = true;
  351. }
  352. }
  353. this.connection.disconnect();
  354. if (this.connection.options.sync !== true) {
  355. this.connection.flush();
  356. }
  357. };
  358. module.exports = XMPP;