cfsolve 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +10 -9
- data/UPSTREAM.md +6 -6
- data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
- data/lib/cfsolve/version.rb +2 -2
- data/vendor/cfsolve/README.md +0 -35
- data/vendor/cfsolve/examples/build_profile.rs +17 -15
- data/vendor/cfsolve/examples/capture_session.rs +37 -0
- data/vendor/cfsolve/examples/check_answers.rs +66 -0
- data/vendor/cfsolve/examples/compare_session.rs +190 -0
- data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
- data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
- data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
- data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
- data/vendor/cfsolve/src/capture/profile.rs +7 -18
- data/vendor/cfsolve/src/capture/recorded.rs +42 -7
- data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
- data/vendor/cfsolve/src/challenge/installed.rs +122 -271
- data/vendor/cfsolve/src/challenge/payload.rs +107 -5
- data/vendor/cfsolve/src/challenge/state.rs +98 -16
- data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
- data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
- data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
- data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
- data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
- data/vendor/cfsolve/src/http/client.rs +11 -0
- data/vendor/cfsolve/src/js/ast.rs +12 -3
- data/vendor/cfsolve/src/js/objects.rs +9 -2
- data/vendor/cfsolve/src/listing/fold.rs +0 -83
- data/vendor/cfsolve/src/listing/query.rs +25 -244
- data/vendor/cfsolve/src/listing.rs +0 -1
- data/vendor/cfsolve/src/session/initial.rs +14 -5
- data/vendor/cfsolve/src/session/probes.rs +160 -125
- data/vendor/cfsolve/src/session/timing.rs +58 -53
- data/vendor/cfsolve/src/session/widget.rs +5 -6
- data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
- data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
- data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
- data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
- data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
- data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
- data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
- data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
- data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
- data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
- data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
- data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
- data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
- data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
- data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
- data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
- data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
- data/vendor/cfsolve/src/stage2/probes.rs +0 -1
- data/vendor/cfsolve/src/stage2/records.rs +4 -13
- data/vendor/cfsolve/src/stage2/reference.json +1 -1
- data/vendor/cfsolve/src/stage2/reference.rs +98 -100
- data/vendor/cfsolve/src/stage2/registry.rs +41 -34
- data/vendor/cfsolve/src/stage2/replay.rs +182 -254
- data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
- data/vendor/cfsolve/src/stage2/trace.rs +18 -0
- data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
- data/vendor/cfsolve/src/stage2.rs +2 -1
- data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
- data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
- data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
- data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
- data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
- data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
- data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
- data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
- data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
- data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
- data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
- data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
- data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
- data/vendor/cfsolve/src/vm/interpret.rs +7 -2
- data/vendor/cfsolve/src/vm/machine.rs +6 -2
- data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
- data/vendor/cfsolve/src/vm/session.rs +36 -14
- data/vendor/cfsolve/src/vm/value.rs +13 -0
- data/vendor/cfsolve/src/vm.rs +1 -0
- data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
- metadata +13 -6
- data/vendor/cfsolve/src/listing/normalize.rs +0 -264
- data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
- data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
- data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
- data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
|
@@ -378,14 +378,6 @@
|
|
|
378
378
|
{
|
|
379
379
|
"candidate": "candidate:3747697418 1 udp 2113937151 f9501b39-1d57-4eb6-8029-16167a5a330b.local 51498 typ host generation 0 ufrag mtbG network-cost 999",
|
|
380
380
|
"observed_ms": 1792876992039
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
"candidate": "candidate:1648342598 1 udp 1677729535 192.0.2.1 64597 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag mtbG network-cost 999",
|
|
384
|
-
"observed_ms": 1792876992052
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
"candidate": "candidate:2007285888 1 udp 1677732095 2001:db8::1 59340 typ srflx raddr :: rport 0 generation 0 ufrag mtbG network-cost 999",
|
|
388
|
-
"observed_ms": 1792876992056
|
|
389
381
|
}
|
|
390
382
|
],
|
|
391
383
|
"data_channel": true,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Measure CSSOM serialization offline, using only a saved capture's inline styles.
|
|
2
|
+
// Usage: node scripts/capture_cssom.mjs <capture.json> <cssom.json>
|
|
3
|
+
// Pass cssom.json as build_profile's optional second argument. The builder verifies
|
|
4
|
+
// the measurements against the hash the captured browser actually reported.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
const [input, output] = process.argv.slice(2);
|
|
11
|
+
if (!input || !output) throw new Error('Supply capture.json and output cssom.json');
|
|
12
|
+
const capture = JSON.parse(fs.readFileSync(input, 'utf8'));
|
|
13
|
+
const styles = [...capture.iframeHtml.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map(match => match[1]);
|
|
14
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-cssom-'));
|
|
15
|
+
try {
|
|
16
|
+
const encoded = Buffer.from(JSON.stringify(styles)).toString('base64');
|
|
17
|
+
const document = `<!doctype html><meta charset="utf-8"><pre id="out"></pre><script>
|
|
18
|
+
const sources = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob('${encoded}'), c => c.charCodeAt(0))));
|
|
19
|
+
const result = sources.map(source => {
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.textContent = source;
|
|
22
|
+
document.head.append(style);
|
|
23
|
+
return { source, rules: Array.from(style.sheet.cssRules, rule => rule.cssText) };
|
|
24
|
+
});
|
|
25
|
+
document.getElementById('out').textContent = JSON.stringify(result);
|
|
26
|
+
</script>`;
|
|
27
|
+
const file = path.join(directory, 'styles.html');
|
|
28
|
+
fs.writeFileSync(file, document);
|
|
29
|
+
const dom = execFileSync(process.env.CHROMIUM || 'chromium', [
|
|
30
|
+
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-background-networking',
|
|
31
|
+
'--host-resolver-rules=MAP * ~NOTFOUND', `--user-data-dir=${path.join(directory, 'profile')}`,
|
|
32
|
+
'--dump-dom', `file://${file}`,
|
|
33
|
+
], { encoding: 'utf8', timeout: 30000, maxBuffer: 4 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
34
|
+
const found = dom.match(/<pre id="out">([\s\S]*?)<\/pre>/);
|
|
35
|
+
if (!found) throw new Error('Chromium did not return CSSOM measurements');
|
|
36
|
+
const decoded = found[1].replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
37
|
+
const result = JSON.parse(decoded);
|
|
38
|
+
if (result.length !== styles.length) throw new Error('CSSOM measurement count differs');
|
|
39
|
+
fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n');
|
|
40
|
+
} finally {
|
|
41
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure saved iframe styles in another browser without replaying a challenge.
|
|
3
|
+
|
|
4
|
+
Usage: serve_cssom.py <output-directory> <capture.json>... [--port 8766]
|
|
5
|
+
Open the printed LAN URL in the browser being profiled.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PAGE = b'''<!doctype html><meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
17
|
+
<title>Safari stylesheet measurement</title>
|
|
18
|
+
<body style="font:18px system-ui;padding:24px"><h1>Stylesheet measurement</h1>
|
|
19
|
+
<p id="status">Measuring this browser...</p>
|
|
20
|
+
<script>
|
|
21
|
+
(async () => {
|
|
22
|
+
const captures = await (await fetch('/styles.json')).json();
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const capture of captures) {
|
|
25
|
+
const frame = document.createElement('iframe');
|
|
26
|
+
frame.style.display = 'none';
|
|
27
|
+
document.body.append(frame);
|
|
28
|
+
const doc = frame.contentDocument;
|
|
29
|
+
const sheets = capture.sources.map(source => {
|
|
30
|
+
const style = doc.createElement('style');
|
|
31
|
+
style.textContent = source;
|
|
32
|
+
doc.head.append(style);
|
|
33
|
+
return {source, rules: Array.from(style.sheet.cssRules, rule => rule.cssText)};
|
|
34
|
+
});
|
|
35
|
+
results.push({name: capture.name, sheets});
|
|
36
|
+
frame.remove();
|
|
37
|
+
}
|
|
38
|
+
const response = await fetch('/measurements', {
|
|
39
|
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
40
|
+
body: JSON.stringify({userAgent: navigator.userAgent, results})
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) throw new Error(await response.text());
|
|
43
|
+
document.getElementById('status').textContent = 'Saved successfully. You can close this page and tell me it is done.';
|
|
44
|
+
})().catch(error => document.getElementById('status').textContent = 'Measurement failed: ' + error.message);
|
|
45
|
+
</script>'''
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
50
|
+
parser.add_argument('output', type=Path)
|
|
51
|
+
parser.add_argument('captures', nargs='+', type=Path)
|
|
52
|
+
parser.add_argument('--port', type=int, default=8766)
|
|
53
|
+
arguments = parser.parse_args()
|
|
54
|
+
arguments.output.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
captures = []
|
|
56
|
+
for index, path in enumerate(arguments.captures):
|
|
57
|
+
capture = json.loads(path.read_text())
|
|
58
|
+
sources = re.findall(r'<style\b[^>]*>([\s\S]*?)</style\s*>', capture['iframeHtml'], re.I)
|
|
59
|
+
if not sources:
|
|
60
|
+
raise ValueError(f'No stylesheets in {path}')
|
|
61
|
+
captures.append({'name': f'{index:02d}-{path.parent.name}', 'sources': sources})
|
|
62
|
+
expected = {capture['name']: capture['sources'] for capture in captures}
|
|
63
|
+
|
|
64
|
+
class Handler(BaseHTTPRequestHandler):
|
|
65
|
+
def reply(self, status, content, content_type='text/plain'):
|
|
66
|
+
self.send_response(status)
|
|
67
|
+
self.send_header('Content-Type', content_type)
|
|
68
|
+
self.send_header('Content-Length', str(len(content)))
|
|
69
|
+
self.send_header('Cache-Control', 'no-store')
|
|
70
|
+
self.end_headers()
|
|
71
|
+
self.wfile.write(content)
|
|
72
|
+
|
|
73
|
+
def do_GET(self):
|
|
74
|
+
if self.path == '/':
|
|
75
|
+
self.reply(200, PAGE, 'text/html; charset=utf-8')
|
|
76
|
+
elif self.path == '/styles.json':
|
|
77
|
+
self.reply(200, json.dumps(captures).encode(), 'application/json')
|
|
78
|
+
else:
|
|
79
|
+
self.reply(404, b'Not found')
|
|
80
|
+
|
|
81
|
+
def do_POST(self):
|
|
82
|
+
if self.path != '/measurements':
|
|
83
|
+
return self.reply(404, b'Not found')
|
|
84
|
+
try:
|
|
85
|
+
length = int(self.headers.get('Content-Length', '0'))
|
|
86
|
+
if not 0 < length < 4_000_000:
|
|
87
|
+
raise ValueError('Invalid measurement size')
|
|
88
|
+
measured = json.loads(self.rfile.read(length))
|
|
89
|
+
if {result['name'] for result in measured['results']} != set(expected):
|
|
90
|
+
raise ValueError('Unexpected capture names')
|
|
91
|
+
for result in measured['results']:
|
|
92
|
+
if [sheet['source'] for sheet in result['sheets']] != expected[result['name']]:
|
|
93
|
+
raise ValueError('Stylesheet sources differ')
|
|
94
|
+
if not all(isinstance(rule, str) for sheet in result['sheets'] for rule in sheet['rules']):
|
|
95
|
+
raise ValueError('Invalid CSS rules')
|
|
96
|
+
# Write only after the entire measurement has validated.
|
|
97
|
+
for result in measured['results']:
|
|
98
|
+
(arguments.output / f"{result['name']}.json").write_text(json.dumps(result['sheets'], indent=2) + '\n')
|
|
99
|
+
(arguments.output / 'browser.json').write_text(json.dumps({'userAgent': measured['userAgent']}, indent=2) + '\n')
|
|
100
|
+
print(f"Saved {len(measured['results'])} captures from {measured['userAgent']}", flush=True)
|
|
101
|
+
self.reply(200, b'Saved')
|
|
102
|
+
except (ValueError, KeyError, TypeError) as error:
|
|
103
|
+
self.reply(400, str(error).encode())
|
|
104
|
+
|
|
105
|
+
server = ThreadingHTTPServer(('0.0.0.0', arguments.port), Handler)
|
|
106
|
+
print(f'Listening on port {arguments.port}; open this computer\'s LAN address in Safari.', flush=True)
|
|
107
|
+
server.serve_forever()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == '__main__':
|
|
111
|
+
main()
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
mod headers;
|
|
14
14
|
|
|
15
15
|
use std::collections::BTreeMap;
|
|
16
|
-
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
17
16
|
|
|
18
17
|
use anyhow::{Context, Result, bail};
|
|
19
18
|
use serde::Serialize;
|
|
@@ -33,6 +32,7 @@ use crate::js::{self, JsObject, JsValue, Payload};
|
|
|
33
32
|
use crate::script::Source;
|
|
34
33
|
use crate::stage2::analysis;
|
|
35
34
|
use crate::stage2::benchmark::WorkerCosts;
|
|
35
|
+
use crate::stage2::probes::ice;
|
|
36
36
|
use crate::stage2::probes::media::Measurement;
|
|
37
37
|
use crate::stage2::registry::{Kind, Operation};
|
|
38
38
|
|
|
@@ -247,7 +247,7 @@ impl Reported {
|
|
|
247
247
|
plan.vocabulary()
|
|
248
248
|
.rewrite_to_reference(probe.position(), &mut observed);
|
|
249
249
|
match fields.observed(&observed, started) {
|
|
250
|
-
Ok(media) => self.media = Some(
|
|
250
|
+
Ok(media) => self.media = Some(host_candidates_only(media)),
|
|
251
251
|
Err(error) => problems.push(error.context("reading the media probe")),
|
|
252
252
|
}
|
|
253
253
|
}
|
|
@@ -373,23 +373,12 @@ fn reported<'payload>(
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
///
|
|
377
|
-
///
|
|
378
|
-
|
|
379
|
-
fn without_addresses(mut media: Measurement) -> Measurement {
|
|
376
|
+
/// Keeps only host candidates, which name the capture machine by mDNS name, so shared profiles do
|
|
377
|
+
/// not reveal its public address. [`crate::stage2::probes::ice::refresh`] reports only these.
|
|
378
|
+
fn host_candidates_only(mut media: Measurement) -> Measurement {
|
|
380
379
|
if let Measurement::Observed { snapshot, .. } = &mut media {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
continue;
|
|
384
|
-
};
|
|
385
|
-
let tokens = line.split(' ').map(|token| match token.parse::<IpAddr>() {
|
|
386
|
-
Ok(address) if address.is_unspecified() => token.to_owned(),
|
|
387
|
-
Ok(IpAddr::V4(_)) => Ipv4Addr::new(192, 0, 2, 1).to_string(),
|
|
388
|
-
Ok(IpAddr::V6(_)) => Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).to_string(),
|
|
389
|
-
Err(_) => token.to_owned(),
|
|
390
|
-
});
|
|
391
|
-
candidate.candidate = JsValue::String(tokens.collect::<Vec<String>>().join(" "));
|
|
392
|
-
}
|
|
380
|
+
let candidates = &mut snapshot.connection.published_candidates;
|
|
381
|
+
candidates.retain(|candidate| candidate.candidate.as_str().is_some_and(ice::is_host));
|
|
393
382
|
}
|
|
394
383
|
media
|
|
395
384
|
}
|
|
@@ -40,6 +40,7 @@ struct CapturedPayload {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
impl CapturedSession {
|
|
43
|
+
/// The iframe document as received, including its stylesheets.
|
|
43
44
|
#[must_use]
|
|
44
45
|
pub fn iframe_html(&self) -> &str {
|
|
45
46
|
&self.iframe_html
|
|
@@ -51,18 +52,46 @@ impl CapturedSession {
|
|
|
51
52
|
|
|
52
53
|
/// Returns the deobfuscated inline script from the iframe.
|
|
53
54
|
pub fn source(&self) -> Result<Source> {
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
Source::try_new(&deobfuscate::deobfuscate(self.javascript()?)?.text)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Like [`Self::source`], but reads the `iframe.deobfuscated.js` that `capture_session`
|
|
59
|
+
/// writes beside `capture_path` when there is one.
|
|
60
|
+
pub fn source_beside(&self, capture_path: &Path) -> Result<Source> {
|
|
61
|
+
let deobfuscated = capture_path.with_file_name("iframe.deobfuscated.js");
|
|
62
|
+
if deobfuscated.exists() {
|
|
63
|
+
Source::try_new(&fs::read_to_string(deobfuscated)?)
|
|
64
|
+
} else {
|
|
65
|
+
self.source()
|
|
66
|
+
}
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/// Returns the probe program from the first POST response, unmasked using the ray ID in the
|
|
59
70
|
/// URL.
|
|
60
71
|
pub fn program(&self) -> Result<Vec<u8>> {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
self.programs()?
|
|
73
|
+
.into_iter()
|
|
74
|
+
.next()
|
|
75
|
+
.context("the capture has no POST")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Returns the program each POST response carries, in order, such as the probe program and
|
|
79
|
+
/// then the checkbox program.
|
|
80
|
+
pub fn programs(&self) -> Result<Vec<Vec<u8>>> {
|
|
81
|
+
self.posts
|
|
82
|
+
.iter()
|
|
83
|
+
.map(|post| {
|
|
84
|
+
let url = Url::parse(&post.url)?;
|
|
85
|
+
let ray = challenge::ray(&url)?;
|
|
86
|
+
let encoded = challenge::decode_response(post.response_body.as_bytes(), ray)?;
|
|
87
|
+
Ok(STANDARD.decode(encoded)?)
|
|
88
|
+
})
|
|
89
|
+
.collect()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Returns the iframe's inline script, before deobfuscation.
|
|
93
|
+
pub fn javascript(&self) -> Result<String> {
|
|
94
|
+
challenge::inline_javascript(&self.iframe_html)
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
/// Returns the payload built before probes run.
|
|
@@ -78,6 +107,12 @@ impl CapturedSession {
|
|
|
78
107
|
.find(|payload| payload.contains_key("1"))
|
|
79
108
|
.context("the capture has no payload with probe records")
|
|
80
109
|
}
|
|
110
|
+
|
|
111
|
+
/// Returns the last payload, such as the checkbox interaction's.
|
|
112
|
+
#[must_use]
|
|
113
|
+
pub fn last_payload(&self) -> Option<&Payload> {
|
|
114
|
+
self.payloads.last().map(|payload| &payload.payload)
|
|
115
|
+
}
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
/// Traffic and complete sessions from one proxy run directory.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//! Audit-only recovery includes initialization's timer callbacks. It does not change the
|
|
2
|
+
//! options, key material, or globals used by the payload encoder.
|
|
3
|
+
|
|
4
|
+
use std::rc::Rc;
|
|
5
|
+
|
|
6
|
+
use swc_core::ecma::ast::BinaryOp;
|
|
7
|
+
|
|
8
|
+
use super::{Held, Installed};
|
|
9
|
+
use crate::js::{JsValue, json};
|
|
10
|
+
use crate::listing::fold::{Frame, Known};
|
|
11
|
+
use crate::stage2::vocabulary;
|
|
12
|
+
use crate::vm::{Effect, Listing, Term};
|
|
13
|
+
|
|
14
|
+
pub(super) fn read(listing: &Listing, installed: &mut Installed) {
|
|
15
|
+
let mut frame = Frame::<Held>::default();
|
|
16
|
+
for (&pc, instruction) in &listing.instructions {
|
|
17
|
+
for path in &instruction.paths {
|
|
18
|
+
frame.begin();
|
|
19
|
+
frame.follow(pc, &path.effects, evaluate, |frame, effect| {
|
|
20
|
+
// The key factory is declared by evaluating a literal function definition,
|
|
21
|
+
// after a Trusted Types wrapper has returned that same source string.
|
|
22
|
+
for argument in effect.call().map_or(&[][..], |(_, arguments)| arguments) {
|
|
23
|
+
if let Some(known) = frame.resolve(argument)
|
|
24
|
+
&& let Some(text) = known.text()
|
|
25
|
+
&& let Some(function) = text.strip_prefix("function ")
|
|
26
|
+
&& let Some((name, _)) = function.split_once('(')
|
|
27
|
+
&& vocabulary::is_hex_name(name)
|
|
28
|
+
{
|
|
29
|
+
installed.audit.insert(name.into(), "N".into());
|
|
30
|
+
installed
|
|
31
|
+
.audit_roles
|
|
32
|
+
.insert(name.into(), "function-factory".into());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let Effect::SetProperty { object, key, value } = effect else {
|
|
36
|
+
return;
|
|
37
|
+
};
|
|
38
|
+
let (Some(object), Some(key)) = (frame.resolve(object), frame.resolve(key)) else {
|
|
39
|
+
return;
|
|
40
|
+
};
|
|
41
|
+
let value = frame.resolve(value);
|
|
42
|
+
if let Known::Ext(Held::AuditArray(array)) = &object
|
|
43
|
+
&& let Known::Literal(JsValue::Number(index)) = &key
|
|
44
|
+
&& *index >= 0.0
|
|
45
|
+
&& index.fract() == 0.0
|
|
46
|
+
{
|
|
47
|
+
let native = if let Some(Known::Ext(Held::NativeFunction(name))) = &value {
|
|
48
|
+
Some(format!("function {name}() {{ [native code] }}"))
|
|
49
|
+
} else {
|
|
50
|
+
None
|
|
51
|
+
};
|
|
52
|
+
array.borrow_mut().insert(*index as usize, native);
|
|
53
|
+
}
|
|
54
|
+
if !matches!(object, Known::Ext(Held::Window)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let Some(name) = key.text().filter(|name| vocabulary::is_hex_name(name)) else {
|
|
58
|
+
return;
|
|
59
|
+
};
|
|
60
|
+
let (role, audit) = match value {
|
|
61
|
+
Some(Known::Ext(Held::AuditArray(array))) => {
|
|
62
|
+
let members: Option<Vec<String>> =
|
|
63
|
+
array.borrow().values().cloned().collect();
|
|
64
|
+
let Some(members) = members else {
|
|
65
|
+
return;
|
|
66
|
+
};
|
|
67
|
+
let role = if members.is_empty() {
|
|
68
|
+
"empty-array"
|
|
69
|
+
} else {
|
|
70
|
+
"native-array"
|
|
71
|
+
};
|
|
72
|
+
(role, members.join(","))
|
|
73
|
+
}
|
|
74
|
+
Some(Known::Ext(Held::GpuPromise)) => ("gpu-adapter", "o".into()),
|
|
75
|
+
Some(Known::Ext(Held::ClockDelta)) => ("clock-delta", String::new()),
|
|
76
|
+
_ => return,
|
|
77
|
+
};
|
|
78
|
+
installed.audit.insert(name.into(), audit);
|
|
79
|
+
installed.audit_roles.insert(name.into(), role.into());
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn evaluate(frame: &Frame<Held>, value: &Term) -> Option<Known<Held>> {
|
|
86
|
+
let string = |term: &Term| match frame.resolve(term) {
|
|
87
|
+
Some(Known::Literal(JsValue::String(text))) => Some(text),
|
|
88
|
+
_ => None,
|
|
89
|
+
};
|
|
90
|
+
match value {
|
|
91
|
+
Term::Array(elements) if elements.is_empty() => {
|
|
92
|
+
return Some(Known::Ext(Held::AuditArray(Rc::default())));
|
|
93
|
+
}
|
|
94
|
+
Term::Get(_, key) => {
|
|
95
|
+
if let Some(name @ ("querySelector" | "querySelectorAll")) =
|
|
96
|
+
frame.resolve(key).as_ref().and_then(Known::text)
|
|
97
|
+
{
|
|
98
|
+
return Some(Known::Ext(Held::NativeFunction(name.into())));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
Term::Call {
|
|
102
|
+
callee, arguments, ..
|
|
103
|
+
} => {
|
|
104
|
+
if let Term::Get(_, method) = &**callee {
|
|
105
|
+
match frame.resolve(method).as_ref().and_then(Known::text) {
|
|
106
|
+
Some("now") => return Some(Known::Ext(Held::Clock)),
|
|
107
|
+
Some("requestAdapter") => return Some(Known::Ext(Held::GpuPromise)),
|
|
108
|
+
_ => {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if let [argument] = arguments.as_slice()
|
|
112
|
+
&& let Some(text) = string(argument)
|
|
113
|
+
&& text.starts_with("function ")
|
|
114
|
+
{
|
|
115
|
+
return Some(Known::Literal(json!(text)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
Term::Binary(BinaryOp::Sub, _, right)
|
|
119
|
+
if matches!(frame.resolve(right), Some(Known::Ext(Held::Clock))) =>
|
|
120
|
+
{
|
|
121
|
+
return Some(Known::Ext(Held::ClockDelta));
|
|
122
|
+
}
|
|
123
|
+
Term::Binary(BinaryOp::Add, left, right) => {
|
|
124
|
+
if let (Some(left), Some(right)) = (string(left), string(right)) {
|
|
125
|
+
return Some(Known::Literal(json!(format!("{left}{right}"))));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
_ => {}
|
|
129
|
+
}
|
|
130
|
+
super::evaluate(frame, value)
|
|
131
|
+
}
|