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.

strophe.jingle.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /* global $, __filename */
  2. import {
  3. SESSION_INITIATE_RECEIVED,
  4. TRANSPORT_REPLACE_START,
  5. TRANSPORT_REPLACE_SUCCESS
  6. } from '../../service/statistics/AnalyticsEvents';
  7. import { getLogger } from 'jitsi-meet-logger';
  8. import { $iq, Strophe } from 'strophe.js';
  9. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  10. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  11. import RandomUtil from '../util/RandomUtil';
  12. import Statistics from '../statistics/statistics';
  13. import JingleSessionPC from './JingleSessionPC';
  14. import ConnectionPlugin from './ConnectionPlugin';
  15. const logger = getLogger(__filename);
  16. // XXX Strophe is build around the idea of chaining function calls so allow long
  17. // function call chains.
  18. /* eslint-disable newline-per-chained-call */
  19. /**
  20. *
  21. */
  22. class JingleConnectionPlugin extends ConnectionPlugin {
  23. /**
  24. * Creates new <tt>JingleConnectionPlugin</tt>
  25. * @param {XMPP} xmpp
  26. * @param {EventEmitter} eventEmitter
  27. * @param {Array<Object>} p2pStunServers an array which is part of the ice
  28. * config passed to the <tt>PeerConnection</tt> with the structure defined
  29. * by the WebRTC standard.
  30. */
  31. constructor(xmpp, eventEmitter, p2pStunServers) {
  32. super();
  33. this.xmpp = xmpp;
  34. this.eventEmitter = eventEmitter;
  35. this.sessions = {};
  36. this.jvbIceConfig = { iceServers: [ ] };
  37. this.p2pIceConfig = { iceServers: [ ] };
  38. if (Array.isArray(p2pStunServers)) {
  39. logger.info('Configured STUN servers: ', p2pStunServers);
  40. this.p2pIceConfig.iceServers = p2pStunServers;
  41. }
  42. this.mediaConstraints = {
  43. mandatory: {
  44. 'OfferToReceiveAudio': true,
  45. 'OfferToReceiveVideo': true
  46. }
  47. // MozDontOfferDataChannel: true when this is firefox
  48. };
  49. }
  50. /**
  51. *
  52. * @param connection
  53. */
  54. init(connection) {
  55. super.init(connection);
  56. this.connection.addHandler(this.onJingle.bind(this),
  57. 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
  58. }
  59. /**
  60. *
  61. * @param iq
  62. */
  63. onJingle(iq) {
  64. const sid = $(iq).find('jingle').attr('sid');
  65. const action = $(iq).find('jingle').attr('action');
  66. const fromJid = iq.getAttribute('from');
  67. // send ack first
  68. const ack = $iq({ type: 'result',
  69. to: fromJid,
  70. id: iq.getAttribute('id')
  71. });
  72. logger.log(`on jingle ${action} from ${fromJid}`, iq);
  73. let sess = this.sessions[sid];
  74. if (action !== 'session-initiate') {
  75. if (!sess) {
  76. ack.attrs({ type: 'error' });
  77. ack.c('error', { type: 'cancel' })
  78. .c('item-not-found', {
  79. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  80. })
  81. .up()
  82. .c('unknown-session', {
  83. xmlns: 'urn:xmpp:jingle:errors:1'
  84. });
  85. logger.warn('invalid session id', iq);
  86. this.connection.send(ack);
  87. return true;
  88. }
  89. // local jid is not checked
  90. if (fromJid !== sess.remoteJid) {
  91. logger.warn(
  92. 'jid mismatch for session id', sid, sess.remoteJid, iq);
  93. ack.attrs({ type: 'error' });
  94. ack.c('error', { type: 'cancel' })
  95. .c('item-not-found', {
  96. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  97. })
  98. .up()
  99. .c('unknown-session', {
  100. xmlns: 'urn:xmpp:jingle:errors:1'
  101. });
  102. this.connection.send(ack);
  103. return true;
  104. }
  105. } else if (sess !== undefined) {
  106. // Existing session with same session id. This might be out-of-order
  107. // if the sess.remoteJid is the same as from.
  108. ack.attrs({ type: 'error' });
  109. ack.c('error', { type: 'cancel' })
  110. .c('service-unavailable', {
  111. xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
  112. })
  113. .up();
  114. logger.warn('duplicate session id', sid, iq);
  115. this.connection.send(ack);
  116. return true;
  117. }
  118. const now = window.performance.now();
  119. // see http://xmpp.org/extensions/xep-0166.html#concepts-session
  120. switch (action) {
  121. case 'session-initiate': {
  122. logger.log('(TIME) received session-initiate:\t', now);
  123. const startMuted = $(iq).find('jingle>startmuted');
  124. if (startMuted && startMuted.length > 0) {
  125. const audioMuted = startMuted.attr('audio');
  126. const videoMuted = startMuted.attr('video');
  127. this.eventEmitter.emit(
  128. XMPPEvents.START_MUTED_FROM_FOCUS,
  129. audioMuted === 'true',
  130. videoMuted === 'true');
  131. }
  132. // FIXME that should work most of the time, but we'd have to
  133. // think how secure it is to assume that user with "focus"
  134. // nickname is Jicofo.
  135. const isP2P = Strophe.getResourceFromJid(fromJid) !== 'focus';
  136. logger.info(
  137. `Marking session from ${fromJid
  138. } as ${isP2P ? '' : '*not*'} P2P`);
  139. sess
  140. = new JingleSessionPC(
  141. $(iq).find('jingle').attr('sid'),
  142. $(iq).attr('to'),
  143. fromJid,
  144. this.connection,
  145. this.mediaConstraints,
  146. isP2P ? this.p2pIceConfig : this.jvbIceConfig,
  147. isP2P,
  148. /* initiator */ false,
  149. this.xmpp.options);
  150. this.sessions[sess.sid] = sess;
  151. this.eventEmitter.emit(XMPPEvents.CALL_INCOMING,
  152. sess, $(iq).find('>jingle'), now);
  153. Statistics.analytics.sendEvent(
  154. SESSION_INITIATE_RECEIVED, { value: now });
  155. break;
  156. }
  157. case 'session-accept': {
  158. this.eventEmitter.emit(
  159. XMPPEvents.CALL_ACCEPTED, sess, $(iq).find('>jingle'));
  160. break;
  161. }
  162. case 'content-modify': {
  163. sess.modifyContents($(iq).find('>jingle'));
  164. break;
  165. }
  166. case 'transport-info': {
  167. this.eventEmitter.emit(
  168. XMPPEvents.TRANSPORT_INFO, sess, $(iq).find('>jingle'));
  169. break;
  170. }
  171. case 'session-terminate': {
  172. logger.log('terminating...', sess.sid);
  173. let reasonCondition = null;
  174. let reasonText = null;
  175. if ($(iq).find('>jingle>reason').length) {
  176. reasonCondition
  177. = $(iq).find('>jingle>reason>:first')[0].tagName;
  178. reasonText = $(iq).find('>jingle>reason>text').text();
  179. }
  180. this.terminate(sess.sid, reasonCondition, reasonText);
  181. this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
  182. sess, reasonCondition, reasonText);
  183. break;
  184. }
  185. case 'transport-replace':
  186. logger.info('(TIME) Start transport replace', now);
  187. Statistics.analytics.sendEvent(
  188. TRANSPORT_REPLACE_START,
  189. { value: now });
  190. sess.replaceTransport($(iq).find('>jingle'), () => {
  191. const successTime = window.performance.now();
  192. logger.info('(TIME) Transport replace success!', successTime);
  193. Statistics.analytics.sendEvent(
  194. TRANSPORT_REPLACE_SUCCESS,
  195. { value: successTime });
  196. }, error => {
  197. GlobalOnErrorHandler.callErrorHandler(error);
  198. logger.error('Transport replace failed', error);
  199. sess.sendTransportReject();
  200. });
  201. break;
  202. case 'addsource': // FIXME: proprietary, un-jingleish
  203. case 'source-add': // FIXME: proprietary
  204. sess.addRemoteStream($(iq).find('>jingle>content'));
  205. break;
  206. case 'removesource': // FIXME: proprietary, un-jingleish
  207. case 'source-remove': // FIXME: proprietary
  208. sess.removeRemoteStream($(iq).find('>jingle>content'));
  209. break;
  210. default:
  211. logger.warn('jingle action not implemented', action);
  212. ack.attrs({ type: 'error' });
  213. ack.c('error', { type: 'cancel' })
  214. .c('bad-request',
  215. { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
  216. .up();
  217. break;
  218. }
  219. this.connection.send(ack);
  220. return true;
  221. }
  222. /**
  223. * Creates new <tt>JingleSessionPC</tt> meant to be used in a direct P2P
  224. * connection, configured as 'initiator'.
  225. * @param {string} me our JID
  226. * @param {string} peer remote participant's JID
  227. * @return {JingleSessionPC}
  228. */
  229. newP2PJingleSession(me, peer) {
  230. const sess
  231. = new JingleSessionPC(
  232. RandomUtil.randomHexString(12),
  233. me,
  234. peer,
  235. this.connection,
  236. this.mediaConstraints,
  237. this.p2pIceConfig,
  238. /* P2P */ true,
  239. /* initiator */ true,
  240. this.xmpp.options);
  241. this.sessions[sess.sid] = sess;
  242. return sess;
  243. }
  244. /**
  245. *
  246. * @param sid
  247. * @param reasonCondition
  248. * @param reasonText
  249. */
  250. terminate(sid, reasonCondition, reasonText) {
  251. if (this.sessions.hasOwnProperty(sid)) {
  252. if (this.sessions[sid].state !== 'ended') {
  253. this.sessions[sid].onTerminated(reasonCondition, reasonText);
  254. }
  255. delete this.sessions[sid];
  256. }
  257. }
  258. /**
  259. *
  260. */
  261. getStunAndTurnCredentials() {
  262. // get stun and turn configuration from server via xep-0215
  263. // uses time-limited credentials as described in
  264. // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
  265. //
  266. // See https://code.google.com/p/prosody-modules/source/browse/
  267. // mod_turncredentials/mod_turncredentials.lua
  268. // for a prosody module which implements this.
  269. //
  270. // Currently, this doesn't work with updateIce and therefore credentials
  271. // with a long validity have to be fetched before creating the
  272. // peerconnection.
  273. // TODO: implement refresh via updateIce as described in
  274. // https://code.google.com/p/webrtc/issues/detail?id=1650
  275. this.connection.sendIQ(
  276. $iq({ type: 'get',
  277. to: this.connection.domain })
  278. .c('services', { xmlns: 'urn:xmpp:extdisco:1' })
  279. .c('service', { host: `turn.${this.connection.domain}` }),
  280. res => {
  281. const iceservers = [];
  282. $(res).find('>services>service').each((idx, el) => {
  283. // eslint-disable-next-line no-param-reassign
  284. el = $(el);
  285. const dict = {};
  286. const type = el.attr('type');
  287. switch (type) {
  288. case 'stun':
  289. dict.url = `stun:${el.attr('host')}`;
  290. if (el.attr('port')) {
  291. dict.url += `:${el.attr('port')}`;
  292. }
  293. iceservers.push(dict);
  294. break;
  295. case 'turn':
  296. case 'turns': {
  297. dict.url = `${type}:`;
  298. const username = el.attr('username');
  299. // https://code.google.com/p/webrtc/issues/detail
  300. // ?id=1508
  301. if (username) {
  302. const match
  303. = navigator.userAgent.match(
  304. /Chrom(e|ium)\/([0-9]+)\./);
  305. if (match && parseInt(match[2], 10) < 28) {
  306. dict.url += `${username}@`;
  307. } else {
  308. // only works in M28
  309. dict.username = username;
  310. }
  311. }
  312. dict.url += el.attr('host');
  313. const port = el.attr('port');
  314. if (port && port !== '3478') {
  315. dict.url += `:${el.attr('port')}`;
  316. }
  317. const transport = el.attr('transport');
  318. if (transport && transport !== 'udp') {
  319. dict.url += `?transport=${transport}`;
  320. }
  321. dict.credential = el.attr('password')
  322. || dict.credential;
  323. iceservers.push(dict);
  324. break;
  325. }
  326. }
  327. });
  328. const options = this.xmpp.options;
  329. if (options.useStunTurn) {
  330. this.jvbIceConfig.iceServers = iceservers;
  331. }
  332. if (options.p2p && options.p2p.useStunTurn) {
  333. this.p2pIceConfig.iceServers = iceservers;
  334. }
  335. }, err => {
  336. logger.warn('getting turn credentials failed', err);
  337. logger.warn('is mod_turncredentials or similar installed?');
  338. });
  339. // implement push?
  340. }
  341. /**
  342. * Returns the data saved in 'updateLog' in a format to be logged.
  343. */
  344. getLog() {
  345. const data = {};
  346. Object.keys(this.sessions).forEach(sid => {
  347. const session = this.sessions[sid];
  348. const pc = session.peerconnection;
  349. if (pc && pc.updateLog) {
  350. // FIXME: should probably be a .dump call
  351. data[`jingle_${sid}`] = {
  352. updateLog: pc.updateLog,
  353. stats: pc.stats,
  354. url: window.location.href
  355. };
  356. }
  357. });
  358. return data;
  359. }
  360. }
  361. /* eslint-enable newline-per-chained-call */
  362. /**
  363. *
  364. * @param XMPP
  365. * @param eventEmitter
  366. * @param p2pStunServers
  367. */
  368. export default function initJingle(XMPP, eventEmitter, p2pStunServers) {
  369. Strophe.addConnectionPlugin(
  370. 'jingle',
  371. new JingleConnectionPlugin(XMPP, eventEmitter, p2pStunServers));
  372. }