@ai-ecoverse/slicc-browsers 1.16.1 → 1.17.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.
@@ -20,6 +20,12 @@ const decodedBase64 = (value) =>
20
20
  base64Slices(text).map(decodeBase64)
21
21
  );
22
22
  const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
23
+ const gone = (out) =>
24
+ !out.ok &&
25
+ !refused(out) &&
26
+ /session with given id not found|no session with given id|\bsession (?:has been |is |was )?(?:closed|detached)|\btarget closed|target page, context or browser has been closed|\b(?:cdp|connection|transport|websocket|socket) (?:has been |is |was )?closed|websocket is not open/i.test(
27
+ out.error?.message ?? ''
28
+ );
23
29
  const redirects = [301, 302, 303, 307, 308];
24
30
  const redirectHeaders = (to) => ({
25
31
  location: to,
@@ -85,6 +91,7 @@ class RedirectProbe {
85
91
  this.messagesStart = this.client.messages?.length;
86
92
  this.eventsStart = this.client.events.length;
87
93
  this.cursor = this.eventsStart;
94
+ this.detachScan = this.eventsStart;
88
95
  this.responses = [];
89
96
  this.attempts = [];
90
97
  this.jobs = new Set();
@@ -107,8 +114,41 @@ class RedirectProbe {
107
114
  : this.protectedURLs;
108
115
  }
109
116
 
117
+ ended() {
118
+ const events = this.client.events;
119
+ for (; !this.end && this.detachScan < events.length; this.detachScan++) {
120
+ const event = events[this.detachScan];
121
+ if (event.method === 'Target.detachedFromTarget' && event.params?.sessionId === this.session)
122
+ this.end = 'Target.detachedFromTarget';
123
+ }
124
+ return Boolean(this.end);
125
+ }
126
+
127
+ cut() {
128
+ return Boolean(this.end) && !this.complete;
129
+ }
130
+
131
+ async lost() {
132
+ if (!this.ended()) await this.send('Runtime.evaluate', { expression: 'void 0' });
133
+ return this.ended();
134
+ }
135
+
136
+ answers(method) {
137
+ const all = this.attempts.filter((entry) => entry.method === method);
138
+ const answered = all.filter((entry) => !(this.end && (entry.pending || entry.ended)));
139
+ const short =
140
+ Boolean(this.end) && (!this.complete || !answered.length || answered.length < all.length);
141
+ return { answered, short };
142
+ }
143
+
110
144
  async send(method, params = {}) {
145
+ if (this.ended())
146
+ return { ok: false, ended: true, error: { message: `session ended (${this.end})` } };
111
147
  const out = await outcome(this.client.send(method, params, this.session));
148
+ if (gone(out)) {
149
+ this.end ??= out.error.message;
150
+ return { ...out, ended: true };
151
+ }
112
152
  this.responses.push({ method, ...out });
113
153
  return out;
114
154
  }
@@ -151,7 +191,7 @@ class RedirectProbe {
151
191
 
152
192
  async networkBody(requestId, source) {
153
193
  this.learned[source].add(requestId);
154
- if (this.networkBodies.has(requestId)) return;
194
+ if (this.networkBodies.has(requestId) || this.ended()) return;
155
195
  this.networkBodies.add(requestId);
156
196
  const entry = { method: 'Network.getResponseBody', source, pending: true };
157
197
  this.attempts.push(entry);
@@ -191,15 +231,17 @@ class RedirectProbe {
191
231
  carriers.filter((entry) => entry === carrier).length,
192
232
  ])
193
233
  );
194
- const attempts = this.attempts.filter((entry) => entry.method === 'Network.getResponseBody');
234
+ const { answered: attempts, short } = this.answers('Network.getResponseBody');
195
235
  const refusedAll = attempts.length > 0 && attempts.every(refused);
196
236
  const proven = path === 'network' || this.side !== 'guarded' || this.controls?.networkEvents;
197
237
  const reason =
198
238
  path === 'none'
199
239
  ? 'no network id for the redirect'
200
- : proven
201
- ? undefined
202
- : 'control did not prove the redirect Network events';
240
+ : !proven
241
+ ? 'control did not prove the redirect Network events'
242
+ : short
243
+ ? `session ended (${this.end}) before every network body read was answered`
244
+ : undefined;
203
245
  if (reason && (!attempts.length || refusedAll))
204
246
  return {
205
247
  networkBodyPath: path,
@@ -254,11 +296,12 @@ class RedirectProbe {
254
296
  async land(url, start) {
255
297
  const path = new URL(url).pathname;
256
298
  await pause(250);
257
- if (this.stalled) return;
299
+ if (this.stalled || this.ended()) return;
258
300
  const landed = await waitFor(
259
301
  async () =>
260
- (await this.fixture.log()).some((entry) => entry.url.includes(path)) &&
261
- !this.open(this.client.events.slice(start), (seen) => seen === url).length,
302
+ this.ended() ||
303
+ ((await this.fixture.log()).some((entry) => entry.url.includes(path)) &&
304
+ !this.open(this.client.events.slice(start), (seen) => seen === url).length),
262
305
  Math.round(2000 * this.scale),
263
306
  50
264
307
  );
@@ -267,7 +310,7 @@ class RedirectProbe {
267
310
 
268
311
  async terminated() {
269
312
  await waitFor(
270
- () => !this.open(this.client.events.slice(this.eventsStart)).length,
313
+ () => this.ended() || !this.open(this.client.events.slice(this.eventsStart)).length,
271
314
  Math.round(5000 * this.scale),
272
315
  50
273
316
  );
@@ -300,13 +343,15 @@ class RedirectProbe {
300
343
  );
301
344
  const deliveries = Object.values(hops).map((entry) => entry.delivery);
302
345
  const open = Object.keys(hops).filter((id) => hops[id].delivery === 'unterminated');
303
- const missing = open.length > 0 && this.side === 'control';
346
+ const missing = (open.length > 0 && (this.side === 'control' || this.end)) || this.cut();
347
+ const who = this.end ? `session ended (${this.end}) with requests` : 'control requests';
348
+ const detail = open.length
349
+ ? `${who} ${open.join(', ')} unterminated`
350
+ : `session ended (${this.end}) before every request was issued`;
304
351
  return {
305
352
  hops,
306
353
  requestsTerminated: missing ? 'incomplete' : !open.length,
307
- ...(missing
308
- ? { incomplete: `redirect-secret: control requests ${open.join(', ')} unterminated` }
309
- : {}),
354
+ ...(missing ? { incomplete: `redirect-secret: ${detail}` } : {}),
310
355
  hopsRedacted: !deliveries.some((delivery) => ['raw', 'ordinary'].includes(delivery)),
311
356
  };
312
357
  }
@@ -388,6 +433,7 @@ class RedirectProbe {
388
433
  async headers() {
389
434
  for (const [index, to] of this.destinations.entries()) {
390
435
  for (const kind of ['navigate', 'href', 'fetch', 'xhr']) {
436
+ if (this.ended()) return;
391
437
  const code = this.mode === 'mix' ? [301, 303, 307, 308][index] : 302;
392
438
  const url = await this.register(`${index}-${kind}`, to, code);
393
439
  const start = this.client.events.length;
@@ -439,9 +485,9 @@ class RedirectProbe {
439
485
  const snapshot = await this.send('Page.captureSnapshot', { format: 'mhtml' });
440
486
  const dom = await this.send('DOMSnapshot.captureSnapshot', { computedStyles: [] });
441
487
  this.responses.push({
442
- resourceContentAttempted: true,
443
- snapshotAttempted: true,
444
- domSnapshotAttempted: true,
488
+ resourceContentAttempted: !resource.ended,
489
+ snapshotAttempted: !snapshot.ended,
490
+ domSnapshotAttempted: !dom.ended,
445
491
  ordinaryResource: this.contains(resource),
446
492
  ordinarySnapshot: this.contains(snapshot),
447
493
  ordinaryDOM: this.contains(dom),
@@ -455,16 +501,21 @@ class RedirectProbe {
455
501
  await this.send('Runtime.evaluate', {
456
502
  expression: `fetch(${JSON.stringify(this.holdURL)}).catch(()=>{});void 0`,
457
503
  });
458
- if (!(await waitFor(() => Boolean(this.held), 5000, 10)))
504
+ await waitFor(() => Boolean(this.held) || this.ended(), 5000, 10);
505
+ if (!this.held) {
506
+ this.holdURL = undefined;
507
+ if (await this.lost()) return;
459
508
  throw new Error('redirect body: Fetch override pause missing');
509
+ }
460
510
  const requestId = this.held.params.requestId;
461
511
  const out = await this.send('Fetch.continueRequest', { requestId, url });
512
+ this.holdURL = undefined;
513
+ if (out.ended) return;
462
514
  this.responses.push({
463
515
  overrideMethod: 'Fetch.continueRequest',
464
516
  overrideRefused: refused(out),
465
517
  overrideWorked: out.ok,
466
518
  });
467
- this.holdURL = undefined;
468
519
  if (!out.ok) await this.send('Fetch.continueRequest', { requestId });
469
520
  await pause(500);
470
521
  }
@@ -475,6 +526,15 @@ class RedirectProbe {
475
526
  const tree = await this.send('Page.getFrameTree');
476
527
  const frameId = tree.result?.frameTree.frame.id;
477
528
  const start = (await this.fixture.log()).length;
529
+ await this.reads(url, to, frameId);
530
+ this.responses.push({
531
+ cookieSent: (await this.fixture.log())
532
+ .slice(start)
533
+ .some((entry) => entry.headers.cookie?.includes(`${this.cookie}=`)),
534
+ });
535
+ }
536
+
537
+ async reads(url, to, frameId) {
478
538
  const resource = await this.send('Network.loadNetworkResource', {
479
539
  frameId,
480
540
  url,
@@ -496,18 +556,21 @@ class RedirectProbe {
496
556
  event.params.type === 'XHR' &&
497
557
  event.params.request.url === url
498
558
  );
499
- if (!xhr) throw new Error('redirect body: XHR request was not observed');
500
- await this.send('Network.replayXHR', { requestId: xhr.params.requestId });
501
- this.responses.push({ replayAttempted: true });
559
+ if (!xhr) {
560
+ if (await this.lost()) return;
561
+ throw new Error('redirect body: XHR request was not observed');
562
+ }
563
+ const replay = await this.send('Network.replayXHR', { requestId: xhr.params.requestId });
564
+ this.responses.push({ replayAttempted: !replay.ended });
502
565
  await pause(500);
503
- await this.snapshots(url, to, frameId);
504
- await this.iframe();
505
- await this.override(url);
506
- this.responses.push({
507
- cookieSent: (await this.fixture.log())
508
- .slice(start)
509
- .some((entry) => entry.headers.cookie?.includes(`${this.cookie}=`)),
510
- });
566
+ for (const step of [
567
+ () => this.snapshots(url, to, frameId),
568
+ () => this.iframe(),
569
+ () => this.override(url),
570
+ ]) {
571
+ if (this.ended()) return;
572
+ await step();
573
+ }
511
574
  }
512
575
 
513
576
  async iframe() {
@@ -521,15 +584,15 @@ class RedirectProbe {
521
584
  const snapshot = await this.send('Page.captureSnapshot', { format: 'mhtml' });
522
585
  const dom = await this.send('DOMSnapshot.captureSnapshot', { computedStyles: [] });
523
586
  this.responses.push({
524
- iframeSnapshotAttempted: true,
587
+ iframeSnapshotAttempted: !snapshot.ended && !dom.ended,
525
588
  ordinaryIframeSnapshot: this.contains(snapshot),
526
589
  ordinaryIframeDOM: this.contains(dom),
527
590
  });
528
591
  }
529
592
 
530
593
  async evidence() {
531
- await pause(8000);
532
- this.tap();
594
+ if (!this.ended()) await pause(8000);
595
+ if (!this.ended()) this.tap();
533
596
  if (this.side === 'control' && this.controls)
534
597
  this.controls.networkEvents = this.learned.network.size > 0;
535
598
  await this.send('Fetch.disable');
@@ -560,20 +623,24 @@ class RedirectProbe {
560
623
  event.method === 'Network.responseReceivedExtraInfo' &&
561
624
  redirects.includes(event.params.statusCode)
562
625
  );
626
+ const extraHeld = extra.every((event) => {
627
+ const headers = event.params.headers;
628
+ const origin = headers.location ?? headers.Location;
629
+ return (
630
+ this.protectedURLs.some((url) => new URL(url).origin === origin) &&
631
+ headersMatch(headers, origin) &&
632
+ headersTextMatch(event.params.headersText, origin)
633
+ );
634
+ });
563
635
  const extraInPlace =
564
- extra.length >= this.destinations.length * 4 &&
565
- extra.every((event) => {
566
- const headers = event.params.headers;
567
- const origin = headers.location ?? headers.Location;
568
- return (
569
- this.protectedURLs.some((url) => new URL(url).origin === origin) &&
570
- headersMatch(headers, origin) &&
571
- headersTextMatch(event.params.headersText, origin)
572
- );
573
- });
636
+ extraHeld &&
637
+ (extra.length >= this.destinations.length * 4 || (this.end ? 'incomplete' : false));
638
+ const unseen = this.end ? 'incomplete' : false;
574
639
  const found = (key) => this.responses.some((entry) => entry[key]);
575
- const fetchAttempts = this.attempts.filter((entry) => entry.method === 'Fetch.getResponseBody');
576
- return {
640
+ const fetchReads = this.answers('Fetch.getResponseBody');
641
+ const fetchLeaked = fetchReads.answered.some((entry) => !refused(entry));
642
+ const overrides = this.responses.filter((entry) => entry.overrideMethod);
643
+ const evidence = {
577
644
  live: (await this.fixture.log()).some((entry) =>
578
645
  entry.url.includes(`/redirect-secret/${this.tag}`)
579
646
  ),
@@ -587,8 +654,13 @@ class RedirectProbe {
587
654
  ),
588
655
  secretHidden: !replyLeaks.includes(true),
589
656
  sessionSurvives: survives.ok && survives.result?.result?.value === 'doc',
590
- fetchBodyAttempted: fetchAttempts.length > 0,
591
- fetchBodyRefused: fetchAttempts.length > 0 && fetchAttempts.every(refused),
657
+ sessionEnded: this.ended(),
658
+ fetchBodyAttempted: fetchReads.short ? 'incomplete' : fetchReads.answered.length > 0,
659
+ fetchBodyRefused: fetchLeaked
660
+ ? false
661
+ : fetchReads.short
662
+ ? 'incomplete'
663
+ : fetchReads.answered.length > 0,
592
664
  ...this.legs(events),
593
665
  extraInPlace,
594
666
  ...redirectFragments({
@@ -632,26 +704,23 @@ class RedirectProbe {
632
704
  'iframeSnapshotAttempted',
633
705
  'ordinaryIframeSnapshot',
634
706
  'ordinaryIframeDOM',
635
- ].map((key) => [key, found(key)])
707
+ ].map((key) => [key, found(key) || unseen])
636
708
  ),
637
- overridesRefused:
638
- this.responses.some((entry) => entry.overrideMethod) &&
639
- this.responses
640
- .filter((entry) => entry.overrideMethod)
641
- .every((entry) => entry.overrideRefused),
642
- overridesWorked:
643
- this.responses.some((entry) => entry.overrideMethod) &&
644
- this.responses
645
- .filter((entry) => entry.overrideMethod)
646
- .every((entry) => entry.overrideWorked),
709
+ overridesRefused: overrides.length
710
+ ? overrides.every((entry) => entry.overrideRefused)
711
+ : unseen,
712
+ overridesWorked: overrides.length ? overrides.every((entry) => entry.overrideWorked) : unseen,
647
713
  };
714
+ if (!this.end || !Object.values(evidence).includes('incomplete')) return evidence;
715
+ const incomplete = [evidence.incomplete, `redirect-secret: session ended (${this.end})`];
716
+ return { ...evidence, incomplete: incomplete.filter(Boolean).join('; ') };
648
717
  }
649
718
 
650
719
  async close() {
651
720
  this.stopped = true;
652
721
  await this.watching;
653
722
  await this.settle();
654
- await outcome(this.client.send('Fetch.disable', {}, this.session));
723
+ if (!this.ended()) await outcome(this.client.send('Fetch.disable', {}, this.session));
655
724
  if (this.observer) {
656
725
  await outcome(
657
726
  this.reader.send(
@@ -680,7 +749,11 @@ export async function redirectSecret(options) {
680
749
  await probe.setup();
681
750
  if (options.mode === 'body') await probe.body();
682
751
  else await probe.headers();
683
- return await probe.evidence();
752
+ probe.complete = !probe.ended();
753
+ const evidence = await probe.evidence();
754
+ if (probe.end && options.side !== 'guarded')
755
+ throw new Error(`redirect-secret: ${options.side} session ended (${probe.end})`);
756
+ return evidence;
684
757
  } finally {
685
758
  await probe.close();
686
759
  }
@@ -88,7 +88,8 @@
88
88
  "chromeUrlsAttach",
89
89
  "extensionWorld",
90
90
  "discovery",
91
- "clientExit"
91
+ "clientExit",
92
+ "siteIsolationOff"
92
93
  ],
93
94
  "properties": {
94
95
  "autoAttach": {
@@ -109,7 +110,8 @@
109
110
  "type": "string",
110
111
  "minLength": 1
111
112
  },
112
- "clientExit": { "type": "string", "minLength": 1 }
113
+ "clientExit": { "type": "string", "minLength": 1 },
114
+ "siteIsolationOff": { "type": "string", "minLength": 1 }
113
115
  }
114
116
  },
115
117
  "placeholders": {
@@ -139,7 +141,8 @@
139
141
  "chromeUrlsAttach",
140
142
  "extensionWorld",
141
143
  "discovery",
142
- "clientExit"
144
+ "clientExit",
145
+ "siteIsolationOff"
143
146
  ]
144
147
  },
145
148
  "requires": {
@@ -453,6 +456,18 @@
453
456
  "as": { "$ref": "#/$defs/name" }
454
457
  }
455
458
  },
459
+ {
460
+ "type": "object",
461
+ "additionalProperties": false,
462
+ "required": ["frameRead", "from", "as"],
463
+ "properties": {
464
+ "frameRead": { "enum": ["blank", "srcdoc"] },
465
+ "from": { "type": "string", "minLength": 1 },
466
+ "on": { "$ref": "#/$defs/on" },
467
+ "requires": { "$ref": "#/$defs/requires" },
468
+ "as": { "$ref": "#/$defs/name" }
469
+ }
470
+ },
456
471
  {
457
472
  "type": "object",
458
473
  "additionalProperties": false,