mockrtc 0.1.0 → 0.3.0

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 (92) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/README.md +2 -2
  3. package/dist/client/mockrtc-admin-request-builder.d.ts +27 -0
  4. package/dist/client/mockrtc-admin-request-builder.js +201 -0
  5. package/dist/client/mockrtc-admin-request-builder.js.map +1 -0
  6. package/dist/client/mockrtc-client.d.ts +12 -4
  7. package/dist/client/mockrtc-client.js +38 -22
  8. package/dist/client/mockrtc-client.js.map +1 -1
  9. package/dist/client/mockrtc-remote-peer.d.ts +3 -2
  10. package/dist/client/mockrtc-remote-peer.js.map +1 -1
  11. package/dist/handling/handler-builder.d.ts +16 -9
  12. package/dist/handling/handler-builder.js +11 -1
  13. package/dist/handling/handler-builder.js.map +1 -1
  14. package/dist/handling/handler-step-definitions.d.ts +43 -25
  15. package/dist/handling/handler-step-definitions.js +61 -19
  16. package/dist/handling/handler-step-definitions.js.map +1 -1
  17. package/dist/handling/handler-steps.d.ts +4 -1
  18. package/dist/handling/handler-steps.js +27 -17
  19. package/dist/handling/handler-steps.js.map +1 -1
  20. package/dist/main-browser.d.ts +3 -0
  21. package/dist/main-browser.js +6 -1
  22. package/dist/main-browser.js.map +1 -1
  23. package/dist/main.d.ts +8 -5
  24. package/dist/main.js +6 -1
  25. package/dist/main.js.map +1 -1
  26. package/dist/matching/matcher-definitions.d.ts +51 -0
  27. package/dist/matching/matcher-definitions.js +94 -0
  28. package/dist/matching/matcher-definitions.js.map +1 -0
  29. package/dist/matching/matchers.d.ts +27 -0
  30. package/dist/matching/matchers.js +87 -0
  31. package/dist/matching/matchers.js.map +1 -0
  32. package/dist/mockrtc-base.d.ts +16 -0
  33. package/dist/mockrtc-base.js +19 -0
  34. package/dist/mockrtc-base.js.map +1 -0
  35. package/dist/mockrtc-peer.d.ts +44 -6
  36. package/dist/mockrtc.d.ts +200 -1
  37. package/dist/mockrtc.js +1 -0
  38. package/dist/mockrtc.js.map +1 -1
  39. package/dist/rule-builder.d.ts +86 -0
  40. package/dist/rule-builder.js +113 -0
  41. package/dist/rule-builder.js.map +1 -0
  42. package/dist/server/mockrtc-admin-plugin.d.ts +2 -2
  43. package/dist/server/mockrtc-admin-plugin.js +165 -3
  44. package/dist/server/mockrtc-admin-plugin.js.map +1 -1
  45. package/dist/server/mockrtc-server-peer.d.ts +8 -2
  46. package/dist/server/mockrtc-server-peer.js +119 -6
  47. package/dist/server/mockrtc-server-peer.js.map +1 -1
  48. package/dist/server/mockrtc-server.d.ts +20 -5
  49. package/dist/server/mockrtc-server.js +87 -14
  50. package/dist/server/mockrtc-server.js.map +1 -1
  51. package/dist/webrtc/datachannel-stream.d.ts +4 -0
  52. package/dist/webrtc/datachannel-stream.js +27 -1
  53. package/dist/webrtc/datachannel-stream.js.map +1 -1
  54. package/dist/webrtc/mediatrack-stream.d.ts +6 -0
  55. package/dist/webrtc/mediatrack-stream.js +28 -2
  56. package/dist/webrtc/mediatrack-stream.js.map +1 -1
  57. package/dist/webrtc/mockrtc-connection.d.ts +1 -1
  58. package/dist/webrtc/mockrtc-connection.js +77 -60
  59. package/dist/webrtc/mockrtc-connection.js.map +1 -1
  60. package/dist/webrtc/rtc-connection.d.ts +28 -5
  61. package/dist/webrtc/rtc-connection.js +62 -23
  62. package/dist/webrtc/rtc-connection.js.map +1 -1
  63. package/dist/webrtc-hooks.js +10 -2
  64. package/dist/webrtc-hooks.js.map +1 -1
  65. package/package.json +12 -6
  66. package/src/client/mockrtc-admin-request-builder.ts +232 -0
  67. package/src/client/mockrtc-client.ts +50 -28
  68. package/src/client/mockrtc-remote-peer.ts +9 -8
  69. package/src/handling/handler-builder.ts +22 -10
  70. package/src/handling/handler-step-definitions.ts +87 -27
  71. package/src/handling/handler-steps.ts +34 -20
  72. package/src/main-browser.ts +5 -0
  73. package/src/main.ts +23 -5
  74. package/src/matching/matcher-definitions.ts +109 -0
  75. package/src/matching/matchers.ts +118 -0
  76. package/src/mockrtc-base.ts +49 -0
  77. package/src/mockrtc-peer.ts +48 -6
  78. package/src/mockrtc.ts +235 -1
  79. package/src/rule-builder.ts +142 -0
  80. package/src/server/mockrtc-admin-plugin.ts +200 -9
  81. package/src/server/mockrtc-server-peer.ts +194 -7
  82. package/src/server/mockrtc-server.ts +136 -19
  83. package/src/webrtc/datachannel-stream.ts +31 -1
  84. package/src/webrtc/mediatrack-stream.ts +31 -4
  85. package/src/webrtc/mockrtc-connection.ts +22 -7
  86. package/src/webrtc/rtc-connection.ts +111 -29
  87. package/src/webrtc-hooks.ts +13 -4
  88. package/test/integration/events.spec.ts +538 -0
  89. package/test/integration/matching.spec.ts +257 -0
  90. package/test/integration/proxy.spec.ts +68 -1
  91. package/test/integration/send-steps.spec.ts +25 -0
  92. package/test/test-setup.ts +19 -0
@@ -0,0 +1,257 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import {
7
+ MockRTC,
8
+ expect,
9
+ delay
10
+ } from '../test-setup';
11
+
12
+ describe("Connection rule matching", () => {
13
+
14
+ const mockRTC = MockRTC.getRemote({ recordMessages: true });
15
+
16
+ beforeEach(() => mockRTC.start());
17
+ afterEach(() => mockRTC.stop());
18
+
19
+ it("by default, matches and proxies all connections", async () => {
20
+ const remoteConn = new RTCPeerConnection();
21
+ const remotelyReceivedMessages: Array<string | Buffer> = [];
22
+
23
+ remoteConn.addEventListener('datachannel', ({ channel }) => {
24
+ channel.addEventListener('message', ({ data }) =>
25
+ remotelyReceivedMessages.push(data)
26
+ );
27
+ });
28
+
29
+ const matchingPeer = await mockRTC.getMatchingPeer();
30
+ // No rules defined!
31
+
32
+ // Create a local data connection:
33
+ const localConn = new RTCPeerConnection();
34
+ MockRTC.hookWebRTCConnection(localConn, matchingPeer); // Automatically redirect traffic via matchingPeer
35
+
36
+ const dataChannel = localConn.createDataChannel("dataChannel");
37
+
38
+ // Create a local offer (which will be hooked automatically):
39
+ const localOffer = await localConn.createOffer();
40
+ localConn.setLocalDescription(localOffer);
41
+
42
+ // v-- Normally happens remotely, via signalling ---
43
+ remoteConn.setRemoteDescription(localOffer);
44
+ const remoteAnswer = await remoteConn.createAnswer();
45
+ remoteConn.setLocalDescription(remoteAnswer);
46
+ // ^-- Normally happens remotely, via signalling ---
47
+
48
+ // Accept the real remote answer, and start communicating:
49
+ localConn.setRemoteDescription(remoteAnswer);
50
+
51
+ await new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
52
+
53
+ dataChannel.send('local message 1');
54
+ dataChannel.send('local message 2');
55
+ dataChannel.send('local message 3');
56
+
57
+ await delay(100);
58
+
59
+ // Traffic is passed through untouched, as expected:
60
+ expect(remotelyReceivedMessages).to.deep.equal([
61
+ 'local message 1',
62
+ 'local message 2',
63
+ 'local message 3'
64
+ ]);
65
+
66
+ // But does go through the proxy:
67
+ expect(await matchingPeer.getAllMessages()).to.deep.equal([
68
+ 'local message 1',
69
+ 'local message 2',
70
+ 'local message 3',
71
+ ]);
72
+ });
73
+
74
+ it("can match data connections", async () => {
75
+ // Explicitly hard-close media connections:
76
+ await mockRTC.forConnections()
77
+ .withMedia()
78
+ .thenClose();
79
+
80
+ // Send a message on data channels only:
81
+ await mockRTC.forConnections()
82
+ .withDataChannels()
83
+ .waitForChannel()
84
+ .thenSend('bye');
85
+
86
+ const matchingPeer = await mockRTC.getMatchingPeer();
87
+
88
+ // Create a local data connection:
89
+ const localConn = new RTCPeerConnection();
90
+
91
+ const dataChannel = localConn.createDataChannel("dataChannel");
92
+
93
+ const messagePromise = new Promise((resolve) => {
94
+ dataChannel.addEventListener('message', ({ data }) => resolve(data));
95
+ });
96
+
97
+ const localOffer = await localConn.createOffer();
98
+ await localConn.setLocalDescription(localOffer);
99
+ const { answer } = await matchingPeer.answerOffer(localOffer);
100
+ await localConn.setRemoteDescription(answer);
101
+
102
+ // Wait until the matching handler sends the configured message:
103
+ const receivedMessage = await messagePromise;
104
+ expect(receivedMessage).to.equal('bye');
105
+ });
106
+
107
+ it("can match media connections", async () => {
108
+ // Explicitly hard-close data connections:
109
+ await mockRTC.forConnections()
110
+ .withDataChannels()
111
+ .thenClose();
112
+
113
+ // Send a message on media connections only:
114
+ await mockRTC.forConnections()
115
+ .withMedia()
116
+ .thenEcho();
117
+
118
+ const matchingPeer = await mockRTC.getMatchingPeer();
119
+
120
+ // Create a local connection:
121
+ const localConn = new RTCPeerConnection();
122
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
123
+ localConn.addTrack(stream.getTracks()[0], stream);
124
+
125
+ // Turn incoming tracks into readable streams of frames:
126
+ const mediaStreamPromise = new Promise<ReadableStream<VideoFrame>>((resolve) => {
127
+ localConn.addEventListener('track', ({ track }) => {
128
+ const streamProcessor = new MediaStreamTrackProcessor({
129
+ track: track as MediaStreamVideoTrack
130
+ });
131
+ resolve(streamProcessor.readable);
132
+ });
133
+ });
134
+
135
+ // Connect the local connection to the matching peer:
136
+ const localOffer = await localConn.createOffer();
137
+ await localConn.setLocalDescription(localOffer);
138
+ const { answer } = await matchingPeer.answerOffer(localOffer);
139
+ await localConn.setRemoteDescription(answer);
140
+
141
+ // Check that our video is mirrored as expected:
142
+ const localMedia = await mediaStreamPromise;
143
+ const { value: localFrame } = await localMedia!.getReader().read();
144
+ expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
145
+ expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
146
+ });
147
+
148
+ it("can match connections by host", async () => {
149
+ // Send a message for connections made from example.com pages:
150
+ await mockRTC.forConnections()
151
+ .fromPageHostname('example.com')
152
+ .waitForChannel()
153
+ .thenSend('hello example.com');
154
+
155
+ // Close any other connections:
156
+ await mockRTC.forConnections()
157
+ .thenClose();
158
+
159
+ const matchingPeer = await mockRTC.getMatchingPeer();
160
+
161
+ // Create a local data connection:
162
+ const localConn = new RTCPeerConnection();
163
+
164
+ const dataChannel = localConn.createDataChannel("dataChannel");
165
+
166
+ const messagePromise = new Promise((resolve) => {
167
+ dataChannel.addEventListener('message', ({ data }) => resolve(data));
168
+ });
169
+
170
+ const localOffer = await localConn.createOffer();
171
+ await localConn.setLocalDescription(localOffer);
172
+ const { answer } = await matchingPeer.answerOffer(localOffer, {
173
+ connectionMetadata: {
174
+ sourceURL: 'https://example.com/abc?x=y#123'
175
+ }
176
+ });
177
+ await localConn.setRemoteDescription(answer);
178
+
179
+ // Wait until the matching handler sends the configured message:
180
+ const receivedMessage = await messagePromise;
181
+ expect(receivedMessage).to.equal('hello example.com');
182
+ });
183
+
184
+ it("can match connections by URL regex", async () => {
185
+ // Close some other connections:
186
+ await mockRTC.forConnections()
187
+ .fromPageUrlMatching(/\?1+1=3/)
188
+ .thenClose();
189
+
190
+ // Send a message for connections made from matching pages:
191
+ await mockRTC.forConnections()
192
+ .fromPageUrlMatching(/\?x=y/)
193
+ .waitForChannel()
194
+ .thenSend('hello x=y');
195
+
196
+ const matchingPeer = await mockRTC.getMatchingPeer();
197
+
198
+ // Create a local data connection:
199
+ const localConn = new RTCPeerConnection();
200
+
201
+ const dataChannel = localConn.createDataChannel("dataChannel");
202
+
203
+ const messagePromise = new Promise((resolve) => {
204
+ dataChannel.addEventListener('message', ({ data }) => resolve(data));
205
+ });
206
+
207
+ const localOffer = await localConn.createOffer();
208
+ await localConn.setLocalDescription(localOffer);
209
+ const { answer } = await matchingPeer.answerOffer(localOffer, {
210
+ connectionMetadata: {
211
+ sourceURL: 'https://example.com/abc?x=y#123'
212
+ }
213
+ });
214
+ await localConn.setRemoteDescription(answer);
215
+
216
+ // Wait until the matching handler sends the configured message:
217
+ const receivedMessage = await messagePromise;
218
+ expect(receivedMessage).to.equal('hello x=y');
219
+ });
220
+
221
+ it("can match connections by user agent regex", async () => {
222
+ // Close some other connections:
223
+ await mockRTC.forConnections()
224
+ .fromUserAgentMatching(/IE6/)
225
+ .thenClose();
226
+
227
+ // Send a message for connections made from Firefox:
228
+ await mockRTC.forConnections()
229
+ .fromUserAgentMatching(/Firefox/)
230
+ .waitForChannel()
231
+ .thenSend('hello Firefox');
232
+
233
+ const matchingPeer = await mockRTC.getMatchingPeer();
234
+
235
+ // Create a local data connection:
236
+ const localConn = new RTCPeerConnection();
237
+
238
+ const dataChannel = localConn.createDataChannel("dataChannel");
239
+
240
+ const messagePromise = new Promise((resolve) => {
241
+ dataChannel.addEventListener('message', ({ data }) => resolve(data));
242
+ });
243
+
244
+ const localOffer = await localConn.createOffer();
245
+ await localConn.setLocalDescription(localOffer);
246
+ const { answer } = await matchingPeer.answerOffer(localOffer, {
247
+ connectionMetadata: {
248
+ userAgent: 'Mozilla/5.0 (Firefox/123)'
249
+ }
250
+ });
251
+ await localConn.setRemoteDescription(answer);
252
+
253
+ // Wait until the matching handler sends the configured message:
254
+ const receivedMessage = await messagePromise;
255
+ expect(receivedMessage).to.equal('hello Firefox');
256
+ });
257
+ });
@@ -3,13 +3,15 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
+ import { MockRTCEventData } from '../../src/mockrtc';
6
7
  import {
7
8
  MockRTC,
8
9
  expect,
9
10
  waitForChannelOpen,
10
11
  waitForChannelClose,
11
12
  waitForState,
12
- setupPerfectNegotiation
13
+ setupPerfectNegotiation,
14
+ getDeferred
13
15
  } from '../test-setup';
14
16
 
15
17
  describe("When proxying WebRTC traffic", () => {
@@ -523,4 +525,69 @@ describe("When proxying WebRTC traffic", () => {
523
525
  expect(remoteFrame!.displayWidth).to.be.greaterThanOrEqual(320);
524
526
  });
525
527
 
528
+ it("should include user-agent & URL metadata when creating a hooked offer", async () => {
529
+ const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
530
+ mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
531
+
532
+ const remoteConn = new RTCPeerConnection();
533
+
534
+ const mockPeer = await mockRTC.buildPeer()
535
+ .thenClose();
536
+
537
+ // Create a local data connection:
538
+ const localConn = new RTCPeerConnection();
539
+ MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
540
+ localConn.createDataChannel("dataChannel");
541
+
542
+ // Create a local offer (which will be hooked automatically):
543
+ const localOffer = await localConn.createOffer();
544
+ localConn.setLocalDescription(localOffer);
545
+
546
+ // v-- Normally happens remotely, via signalling ---
547
+ remoteConn.setRemoteDescription(localOffer);
548
+ const remoteAnswer = await remoteConn.createAnswer();
549
+ remoteConn.setLocalDescription(remoteAnswer);
550
+ // ^-- Normally happens remotely, via signalling ---
551
+
552
+ // Accept the real remote answer, and start communicating:
553
+ localConn.setRemoteDescription(remoteAnswer);
554
+
555
+ // Check that the connection event automatically includes the user agent:
556
+ const connectEvent = await eventPromise;
557
+ expect(connectEvent.metadata.userAgent).to.equal(navigator.userAgent);
558
+ expect(connectEvent.metadata.sourceURL).to.equal(window.location.href);
559
+ });
560
+
561
+ it("should include user-agent & URL metadata when creating a hooked answer", async () => {
562
+ const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
563
+ mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
564
+
565
+ const remoteConn = new RTCPeerConnection();
566
+ remoteConn.createDataChannel("empty-channel"); // Need at least one channel/track to get an offer
567
+
568
+ const mockPeer = await mockRTC.buildPeer()
569
+ .thenClose();
570
+
571
+ // Remote connection starts first, sending us a real offer:
572
+ const remoteOffer = await remoteConn.createOffer();
573
+ remoteConn.setLocalDescription(remoteOffer);
574
+
575
+ // Create a local data connection:
576
+ const localConn = new RTCPeerConnection();
577
+ MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
578
+
579
+ // Receive the remote offer, use that locally to create an answer (this is all hooked):
580
+ await localConn.setRemoteDescription(remoteOffer);
581
+ const localAnswer = await localConn.createAnswer();
582
+ localConn.setLocalDescription(localAnswer);
583
+
584
+ // Signal the answer back to the real unhooked remote connection:
585
+ await remoteConn.setRemoteDescription(localAnswer);
586
+
587
+ // Check that the connection event automatically includes the user agent:
588
+ const connectEvent = await eventPromise;
589
+ expect(connectEvent.metadata.userAgent).to.equal(navigator.userAgent);
590
+ expect(connectEvent.metadata.sourceURL).to.equal(window.location.href);
591
+ });
592
+
526
593
  });
@@ -73,4 +73,29 @@ describe("Send steps", function () {
73
73
  ]);
74
74
  });
75
75
 
76
+ it("should be able to create a new data channel, and send a message there", async () => {
77
+ const mockPeer = await mockRTC.buildPeer()
78
+ .createDataChannel('new-channel')
79
+ .thenSend('Hello from new channel');
80
+
81
+ const localConnection = new RTCPeerConnection();
82
+
83
+ const messagePromise = new Promise((resolve) => {
84
+ localConnection.addEventListener('datachannel', ({ channel }) => {
85
+ expect(channel.label).to.equal('new-channel');
86
+ channel.addEventListener('message', ({ data }) => resolve(data));
87
+ });
88
+ });
89
+
90
+ const { offer, setAnswer } = await mockPeer.createOffer();
91
+ await localConnection.setRemoteDescription(offer);
92
+ const localAnswer = await localConnection.createAnswer();
93
+ await localConnection.setLocalDescription(localAnswer);
94
+ await setAnswer(localAnswer);
95
+
96
+ // Wait for a response:
97
+ const message = await messagePromise;
98
+ expect(message).to.equal('Hello from new channel');
99
+ });
100
+
76
101
  });
@@ -17,6 +17,8 @@ before(async function () {
17
17
  let shownMessage = false;
18
18
 
19
19
  while (true) {
20
+ await delay(100);
21
+
20
22
  try {
21
23
  const server = MockRTC.getRemote();
22
24
  await server.start();
@@ -133,4 +135,21 @@ export function setupPerfectNegotiation(
133
135
  console.error('onmessage error', err);
134
136
  }
135
137
  }
138
+ }
139
+
140
+ export type Deferred<T> = Promise<T> & {
141
+ resolve(value: T): void,
142
+ reject(e: Error): void
143
+ }
144
+ export function getDeferred<T>(): Deferred<T> {
145
+ let resolveCallback: (value: T) => void;
146
+ let rejectCallback: (e: Error) => void;
147
+ let result = <Deferred<T>> new Promise((resolve, reject) => {
148
+ resolveCallback = resolve;
149
+ rejectCallback = reject;
150
+ });
151
+ result.resolve = resolveCallback!;
152
+ result.reject = rejectCallback!;
153
+
154
+ return result;
136
155
  }