@webex/plugin-meetings 3.10.0-next.9 → 3.10.0-webex-services-ready.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +11 -3
  4. package/dist/constants.js.map +1 -1
  5. package/dist/hashTree/constants.js +20 -0
  6. package/dist/hashTree/constants.js.map +1 -0
  7. package/dist/hashTree/hashTree.js +515 -0
  8. package/dist/hashTree/hashTree.js.map +1 -0
  9. package/dist/hashTree/hashTreeParser.js +1266 -0
  10. package/dist/hashTree/hashTreeParser.js.map +1 -0
  11. package/dist/hashTree/types.js +21 -0
  12. package/dist/hashTree/types.js.map +1 -0
  13. package/dist/hashTree/utils.js +48 -0
  14. package/dist/hashTree/utils.js.map +1 -0
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/index.js +511 -48
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/types.js +7 -0
  20. package/dist/locus-info/types.js.map +1 -0
  21. package/dist/meeting/index.js +41 -15
  22. package/dist/meeting/index.js.map +1 -1
  23. package/dist/meeting/util.js +1 -0
  24. package/dist/meeting/util.js.map +1 -1
  25. package/dist/meetings/index.js +112 -70
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/metrics/constants.js +3 -1
  28. package/dist/metrics/constants.js.map +1 -1
  29. package/dist/reachability/clusterReachability.js +44 -358
  30. package/dist/reachability/clusterReachability.js.map +1 -1
  31. package/dist/reachability/reachability.types.js +14 -1
  32. package/dist/reachability/reachability.types.js.map +1 -1
  33. package/dist/reachability/reachabilityPeerConnection.js +445 -0
  34. package/dist/reachability/reachabilityPeerConnection.js.map +1 -0
  35. package/dist/types/constants.d.ts +26 -21
  36. package/dist/types/hashTree/constants.d.ts +8 -0
  37. package/dist/types/hashTree/hashTree.d.ts +129 -0
  38. package/dist/types/hashTree/hashTreeParser.d.ts +260 -0
  39. package/dist/types/hashTree/types.d.ts +25 -0
  40. package/dist/types/hashTree/utils.d.ts +9 -0
  41. package/dist/types/locus-info/index.d.ts +91 -42
  42. package/dist/types/locus-info/types.d.ts +46 -0
  43. package/dist/types/meeting/index.d.ts +22 -9
  44. package/dist/types/meetings/index.d.ts +9 -2
  45. package/dist/types/metrics/constants.d.ts +2 -0
  46. package/dist/types/reachability/clusterReachability.d.ts +10 -88
  47. package/dist/types/reachability/reachability.types.d.ts +12 -1
  48. package/dist/types/reachability/reachabilityPeerConnection.d.ts +111 -0
  49. package/dist/webinar/index.js +1 -1
  50. package/package.json +22 -21
  51. package/src/constants.ts +13 -1
  52. package/src/hashTree/constants.ts +9 -0
  53. package/src/hashTree/hashTree.ts +463 -0
  54. package/src/hashTree/hashTreeParser.ts +1161 -0
  55. package/src/hashTree/types.ts +30 -0
  56. package/src/hashTree/utils.ts +42 -0
  57. package/src/locus-info/index.ts +556 -85
  58. package/src/locus-info/types.ts +48 -0
  59. package/src/meeting/index.ts +58 -26
  60. package/src/meeting/util.ts +1 -0
  61. package/src/meetings/index.ts +104 -51
  62. package/src/metrics/constants.ts +2 -0
  63. package/src/reachability/clusterReachability.ts +50 -347
  64. package/src/reachability/reachability.types.ts +15 -1
  65. package/src/reachability/reachabilityPeerConnection.ts +416 -0
  66. package/test/unit/spec/hashTree/hashTree.ts +655 -0
  67. package/test/unit/spec/hashTree/hashTreeParser.ts +1532 -0
  68. package/test/unit/spec/hashTree/utils.ts +103 -0
  69. package/test/unit/spec/locus-info/index.js +667 -1
  70. package/test/unit/spec/meeting/index.js +91 -20
  71. package/test/unit/spec/meeting/utils.js +77 -0
  72. package/test/unit/spec/meetings/index.js +71 -26
  73. package/test/unit/spec/reachability/clusterReachability.ts +281 -138
@@ -0,0 +1,416 @@
1
+ import {Defer} from '@webex/common';
2
+
3
+ import LoggerProxy from '../common/logs/logger-proxy';
4
+ import {ClusterNode} from './request';
5
+ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
6
+ import EventsScope from '../common/events/events-scope';
7
+
8
+ import {CONNECTION_STATE, ICE_GATHERING_STATE} from '../constants';
9
+ import {
10
+ ClusterReachabilityResult,
11
+ NatType,
12
+ Protocol,
13
+ ReachabilityPeerConnectionEvents,
14
+ } from './reachability.types';
15
+
16
+ /**
17
+ * A class to handle RTCPeerConnection lifecycle and ICE candidate gathering for reachability checks.
18
+ * It will do all the work like PeerConnection lifecycle, candidate processing, result management, and event emission.
19
+ */
20
+ export class ReachabilityPeerConnection extends EventsScope {
21
+ public numUdpUrls: number;
22
+ public numTcpUrls: number;
23
+ public numXTlsUrls: number;
24
+ private pc: RTCPeerConnection | null;
25
+ private defer: Defer;
26
+ private startTimestamp: number;
27
+ private srflxIceCandidates: RTCIceCandidate[] = [];
28
+ private clusterName: string;
29
+ private result: ClusterReachabilityResult;
30
+ private emittedSubnets: Set<string> = new Set();
31
+
32
+ /**
33
+ * Constructor for ReachabilityPeerConnection
34
+ * @param {string} clusterName name of the cluster
35
+ * @param {ClusterNode} clusterInfo information about the media cluster
36
+ */
37
+ constructor(clusterName: string, clusterInfo: ClusterNode) {
38
+ super();
39
+ this.clusterName = clusterName;
40
+ this.numUdpUrls = clusterInfo.udp.length;
41
+ this.numTcpUrls = clusterInfo.tcp.length;
42
+ this.numXTlsUrls = clusterInfo.xtls.length;
43
+
44
+ this.pc = this.createPeerConnection(clusterInfo);
45
+
46
+ this.defer = new Defer();
47
+ this.result = {
48
+ udp: {
49
+ result: 'untested',
50
+ },
51
+ tcp: {
52
+ result: 'untested',
53
+ },
54
+ xtls: {
55
+ result: 'untested',
56
+ },
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Gets total elapsed time, can be called only after start() is called
62
+ * @returns {number} Milliseconds
63
+ */
64
+ private getElapsedTime() {
65
+ return Math.round(performance.now() - this.startTimestamp);
66
+ }
67
+
68
+ /**
69
+ * Generate peerConnection config settings
70
+ * @param {ClusterNode} cluster
71
+ * @returns {RTCConfiguration} peerConnectionConfig
72
+ */
73
+ private static buildPeerConnectionConfig(cluster: ClusterNode): RTCConfiguration {
74
+ const udpIceServers = cluster.udp.map((url) => ({
75
+ username: '',
76
+ credential: '',
77
+ urls: [url],
78
+ }));
79
+
80
+ // STUN servers are contacted only using UDP, so in order to test TCP reachability
81
+ // we pretend that Linus is a TURN server, because we can explicitly say "transport=tcp" in TURN urls.
82
+ // We then check for relay candidates to know if TURN-TCP worked (see registerIceCandidateListener()).
83
+ const tcpIceServers = cluster.tcp.map((urlString: string) => {
84
+ return {
85
+ username: 'webexturnreachuser',
86
+ credential: 'webexturnreachpwd',
87
+ urls: [convertStunUrlToTurn(urlString, 'tcp')],
88
+ };
89
+ });
90
+
91
+ const turnTlsIceServers = cluster.xtls.map((urlString: string) => {
92
+ return {
93
+ username: 'webexturnreachuser',
94
+ credential: 'webexturnreachpwd',
95
+ urls: [convertStunUrlToTurnTls(urlString)],
96
+ };
97
+ });
98
+
99
+ return {
100
+ iceServers: [...udpIceServers, ...tcpIceServers, ...turnTlsIceServers],
101
+ iceCandidatePoolSize: 0,
102
+ iceTransportPolicy: 'all',
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Creates an RTCPeerConnection
108
+ * @param {ClusterNode} clusterInfo information about the media cluster
109
+ * @returns {RTCPeerConnection|null} peerConnection
110
+ */
111
+ private createPeerConnection(clusterInfo: ClusterNode): RTCPeerConnection | null {
112
+ try {
113
+ const config = ReachabilityPeerConnection.buildPeerConnectionConfig(clusterInfo);
114
+
115
+ const peerConnection = new RTCPeerConnection(config);
116
+
117
+ return peerConnection;
118
+ } catch (peerConnectionError) {
119
+ LoggerProxy.logger.warn(
120
+ `Reachability:ReachabilityPeerConnection#createPeerConnection --> Error creating peerConnection:`,
121
+ peerConnectionError
122
+ );
123
+
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * @returns {ClusterReachabilityResult} reachability result for this instance
130
+ */
131
+ getResult() {
132
+ return this.result;
133
+ }
134
+
135
+ /**
136
+ * Closes the peerConnection
137
+ * @returns {void}
138
+ */
139
+ private closePeerConnection() {
140
+ if (this.pc) {
141
+ this.pc.onicecandidate = null;
142
+ this.pc.onicegatheringstatechange = null;
143
+ this.pc.close();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Resolves the defer, indicating that reachability checks for this cluster are completed
149
+ *
150
+ * @returns {void}
151
+ */
152
+ private finishReachabilityCheck() {
153
+ this.defer.resolve();
154
+ }
155
+
156
+ /**
157
+ * Aborts the cluster reachability checks by closing the peer connection
158
+ *
159
+ * @returns {void}
160
+ */
161
+ public abort() {
162
+ const {CLOSED} = CONNECTION_STATE;
163
+
164
+ if (this.pc && this.pc.connectionState !== CLOSED) {
165
+ this.closePeerConnection();
166
+ this.finishReachabilityCheck();
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Adds public IP (client media IPs)
172
+ * @param {string} protocol
173
+ * @param {string} publicIp
174
+ * @returns {void}
175
+ */
176
+ private addPublicIp(protocol: Protocol, publicIp?: string | null) {
177
+ if (!publicIp) {
178
+ return;
179
+ }
180
+
181
+ const result = this.result[protocol];
182
+ let ipAdded = false;
183
+
184
+ if (result.clientMediaIPs) {
185
+ if (!result.clientMediaIPs.includes(publicIp)) {
186
+ result.clientMediaIPs.push(publicIp);
187
+ ipAdded = true;
188
+ }
189
+ } else {
190
+ result.clientMediaIPs = [publicIp];
191
+ ipAdded = true;
192
+ }
193
+
194
+ if (ipAdded) {
195
+ this.emit(
196
+ {
197
+ file: 'reachabilityPeerConnection',
198
+ function: 'addPublicIp',
199
+ },
200
+ ReachabilityPeerConnectionEvents.clientMediaIpsUpdated,
201
+ {
202
+ protocol,
203
+ clientMediaIPs: result.clientMediaIPs,
204
+ }
205
+ );
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Registers a listener for the iceGatheringStateChange event
211
+ *
212
+ * @returns {void}
213
+ */
214
+ private registerIceGatheringStateChangeListener() {
215
+ this.pc.onicegatheringstatechange = () => {
216
+ if (this.pc.iceGatheringState === ICE_GATHERING_STATE.COMPLETE) {
217
+ this.closePeerConnection();
218
+ this.defer.resolve();
219
+ }
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Saves the latency in the result for the given protocol and marks it as reachable,
225
+ * emits the "resultReady" event if this is the first result for that protocol,
226
+ * emits the "clientMediaIpsUpdated" event if we already had a result and only found
227
+ * a new client IP
228
+ *
229
+ * @param {string} protocol
230
+ * @param {number} latency
231
+ * @param {string|null} [publicIp]
232
+ * @param {string|null} [serverIp]
233
+ * @returns {void}
234
+ */
235
+ private saveResult(
236
+ protocol: Protocol,
237
+ latency: number,
238
+ publicIp?: string | null,
239
+ serverIp?: string | null
240
+ ) {
241
+ const result = this.result[protocol];
242
+
243
+ if (result.latencyInMilliseconds === undefined) {
244
+ LoggerProxy.logger.log(
245
+ // @ts-ignore
246
+ `Reachability:ReachabilityPeerConnection#saveResult --> Successfully reached ${this.clusterName} over ${protocol}: ${latency}ms`
247
+ );
248
+ result.latencyInMilliseconds = latency;
249
+ result.result = 'reachable';
250
+ if (publicIp) {
251
+ result.clientMediaIPs = [publicIp];
252
+ }
253
+
254
+ this.emit(
255
+ {
256
+ file: 'reachabilityPeerConnection',
257
+ function: 'saveResult',
258
+ },
259
+ ReachabilityPeerConnectionEvents.resultReady,
260
+ {
261
+ protocol,
262
+ ...result,
263
+ }
264
+ );
265
+ } else {
266
+ this.addPublicIp(protocol, publicIp);
267
+ }
268
+
269
+ if (serverIp) {
270
+ if (!this.emittedSubnets.has(serverIp)) {
271
+ this.emittedSubnets.add(serverIp);
272
+ this.emit(
273
+ {
274
+ file: 'reachabilityPeerConnection',
275
+ function: 'saveResult',
276
+ },
277
+ ReachabilityPeerConnectionEvents.reachedSubnets,
278
+ {
279
+ subnets: [serverIp],
280
+ }
281
+ );
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Determines NAT type by analyzing server reflexive candidate patterns
288
+ * @param {RTCIceCandidate} candidate server reflexive candidate
289
+ * @returns {void}
290
+ */
291
+ private determineNatTypeForSrflxCandidate(candidate: RTCIceCandidate) {
292
+ this.srflxIceCandidates.push(candidate);
293
+
294
+ if (this.srflxIceCandidates.length > 1) {
295
+ const portsFound: Record<string, Set<number>> = {};
296
+
297
+ this.srflxIceCandidates.forEach((c) => {
298
+ const key = `${c.address}:${c.relatedPort}`;
299
+ if (!portsFound[key]) {
300
+ portsFound[key] = new Set();
301
+ }
302
+ portsFound[key].add(c.port);
303
+ });
304
+
305
+ Object.entries(portsFound).forEach(([, ports]) => {
306
+ if (ports.size > 1) {
307
+ // Found candidates with the same address and relatedPort, but different ports
308
+ this.emit(
309
+ {
310
+ file: 'reachabilityPeerConnection',
311
+ function: 'determineNatTypeForSrflxCandidate',
312
+ },
313
+ ReachabilityPeerConnectionEvents.natTypeUpdated,
314
+ {
315
+ natType: NatType.SymmetricNat,
316
+ }
317
+ );
318
+ }
319
+ });
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Registers a listener for the icecandidate event
325
+ *
326
+ * @returns {void}
327
+ */
328
+ private registerIceCandidateListener() {
329
+ this.pc.onicecandidate = (e) => {
330
+ const TURN_TLS_PORT = 443;
331
+ const CANDIDATE_TYPES = {
332
+ SERVER_REFLEXIVE: 'srflx',
333
+ RELAY: 'relay',
334
+ };
335
+
336
+ const latencyInMilliseconds = this.getElapsedTime();
337
+
338
+ if (e.candidate) {
339
+ if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
340
+ let serverIp = null;
341
+ if ('url' in e.candidate) {
342
+ const stunServerUrlRegex = /stun:([\d.]+):\d+/;
343
+
344
+ const match = (e.candidate as any).url.match(stunServerUrlRegex);
345
+ serverIp = match && match[1];
346
+ }
347
+
348
+ this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
349
+
350
+ this.determineNatTypeForSrflxCandidate(e.candidate);
351
+ }
352
+
353
+ if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
354
+ const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
355
+ this.saveResult(protocol, latencyInMilliseconds, null, e.candidate.address);
356
+ }
357
+ }
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Starts the process of doing UDP, TCP, and XTLS reachability checks.
363
+ * @returns {Promise<ClusterReachabilityResult>}
364
+ */
365
+ async start(): Promise<ClusterReachabilityResult> {
366
+ if (!this.pc) {
367
+ LoggerProxy.logger.warn(
368
+ `Reachability:ReachabilityPeerConnection#start --> Error: peerConnection is undefined`
369
+ );
370
+
371
+ return this.result;
372
+ }
373
+
374
+ // Initialize this.result as saying that nothing is reachable.
375
+ // It will get updated as we go along and successfully gather ICE candidates.
376
+ this.result.udp = {
377
+ result: this.numUdpUrls > 0 ? 'unreachable' : 'untested',
378
+ };
379
+ this.result.tcp = {
380
+ result: this.numTcpUrls > 0 ? 'unreachable' : 'untested',
381
+ };
382
+ this.result.xtls = {
383
+ result: this.numXTlsUrls > 0 ? 'unreachable' : 'untested',
384
+ };
385
+
386
+ try {
387
+ const offer = await this.pc.createOffer({offerToReceiveAudio: true});
388
+
389
+ this.startTimestamp = performance.now();
390
+
391
+ // Set up the state change listeners before triggering the ICE gathering
392
+ const gatherIceCandidatePromise = this.gatherIceCandidates();
393
+
394
+ // not awaiting the next call on purpose, because we're not sending the offer anywhere and there won't be any answer
395
+ // we just need to make this call to trigger the ICE gathering process
396
+ this.pc.setLocalDescription(offer);
397
+
398
+ await gatherIceCandidatePromise;
399
+ } catch (error) {
400
+ LoggerProxy.logger.warn(`Reachability:ReachabilityPeerConnection#start --> Error: `, error);
401
+ }
402
+
403
+ return this.result;
404
+ }
405
+
406
+ /**
407
+ * Starts the process of gathering ICE candidates
408
+ * @returns {Promise} promise that's resolved once reachability checks are completed or timeout is reached
409
+ */
410
+ private gatherIceCandidates() {
411
+ this.registerIceGatheringStateChangeListener();
412
+ this.registerIceCandidateListener();
413
+
414
+ return this.defer.promise;
415
+ }
416
+ }