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
|
@@ -11,7 +11,7 @@ use url::Url;
|
|
|
11
11
|
use wreq::ws::message::Message;
|
|
12
12
|
|
|
13
13
|
use super::channel::Submission;
|
|
14
|
-
use super::timing::{self, Schedule, Timer};
|
|
14
|
+
use super::timing::{self, Schedule, Timeline, Timer};
|
|
15
15
|
use super::widget::{Embedding, WidgetSession};
|
|
16
16
|
use crate::browser::{Profile, Solve};
|
|
17
17
|
use crate::challenge::payload;
|
|
@@ -59,8 +59,14 @@ pub(crate) async fn prepare(session: &WidgetSession<'_>, solve: &Solve) -> Resul
|
|
|
59
59
|
let engine = session.client.engine();
|
|
60
60
|
let plan = session.analysis.plan();
|
|
61
61
|
let mut options = session.request.options.clone();
|
|
62
|
-
let
|
|
62
|
+
let mut observed = observe(session, plan, profile).await?;
|
|
63
63
|
observed.page_signals = Some(merged.page_signals.clone());
|
|
64
|
+
let replay = answers.map(|answers| Replay::new(answers, plan));
|
|
65
|
+
let planned = durations(plan, &profile.durations, &observed, replay.as_ref());
|
|
66
|
+
// The stack is needed only for the records, so rebuild it while the probes run.
|
|
67
|
+
let probes = run_probes(session, plan, &options, profile, &mut observed, &planned);
|
|
68
|
+
let ((schedule, fetches), stack) = tokio::try_join!(probes, probe_stack(session, plan))?;
|
|
69
|
+
observed.stack = stack;
|
|
64
70
|
// Resource timings now include the first POST and probe requests.
|
|
65
71
|
if let Some(timing) = &merged.state.timing {
|
|
66
72
|
let mut resources = merged.resources.clone();
|
|
@@ -82,17 +88,6 @@ pub(crate) async fn prepare(session: &WidgetSession<'_>, solve: &Solve) -> Resul
|
|
|
82
88
|
}
|
|
83
89
|
window.extend(profile.window.clone());
|
|
84
90
|
|
|
85
|
-
let replay = answers
|
|
86
|
-
.map(|answers| Replay::new(answers, plan, &session.fetched.source, &session.installed))
|
|
87
|
-
.transpose()?;
|
|
88
|
-
let pending_ms = i64::from(profile.pending_ms);
|
|
89
|
-
let schedule = timing::schedule(
|
|
90
|
-
&durations(plan, &profile.durations, &observed, replay.as_ref()),
|
|
91
|
-
profile.startup_ms.into(),
|
|
92
|
-
pending_ms,
|
|
93
|
-
session.arrived_ms,
|
|
94
|
-
performance_clock::epoch_ms(),
|
|
95
|
-
);
|
|
96
91
|
if let Some((probe, Operation::PerformanceEntries { fields })) =
|
|
97
92
|
plan.operation(Kind::PerformanceEntries)
|
|
98
93
|
{
|
|
@@ -107,15 +102,19 @@ pub(crate) async fn prepare(session: &WidgetSession<'_>, solve: &Solve) -> Resul
|
|
|
107
102
|
engine,
|
|
108
103
|
));
|
|
109
104
|
}
|
|
105
|
+
let mut globals = merged.audit_globals.0.clone();
|
|
106
|
+
globals.extend(replay::Globals::recover(&session.fetched.source).0);
|
|
107
|
+
let globals = replay::Globals(globals);
|
|
110
108
|
let stored = replay::Session {
|
|
111
|
-
iframe_url: session.fetched.iframe.url.as_str(),
|
|
112
109
|
iframe_html: &session.fetched.iframe.html,
|
|
110
|
+
iframe_url: session.fetched.iframe.url.as_str(),
|
|
113
111
|
modified: Local::now().format("%m/%d/%Y %H:%M:%S").to_string(),
|
|
114
112
|
referrer: session.fetched.referrer.as_deref().unwrap_or_default(),
|
|
115
113
|
navigation: &session.fetched.iframe.navigation,
|
|
116
114
|
options: &options,
|
|
117
115
|
trace: Trace::recover(&session.fetched.source),
|
|
118
116
|
controller: plan.controller_trace.as_deref(),
|
|
117
|
+
globals: &globals,
|
|
119
118
|
};
|
|
120
119
|
let records = records(
|
|
121
120
|
plan,
|
|
@@ -133,14 +132,14 @@ pub(crate) async fn prepare(session: &WidgetSession<'_>, solve: &Solve) -> Resul
|
|
|
133
132
|
},
|
|
134
133
|
records,
|
|
135
134
|
&profile.document,
|
|
136
|
-
pending_ms,
|
|
135
|
+
profile.pending_ms.into(),
|
|
137
136
|
)?;
|
|
138
137
|
ensure!(
|
|
139
138
|
plaintext.len() > plan.probes().len(),
|
|
140
139
|
"the assembled payload holds nothing besides records"
|
|
141
140
|
);
|
|
142
141
|
payload::append_send_time_at(&mut plaintext, &merged.state, &options, schedule.send_at);
|
|
143
|
-
let geometry = window_geometry(replay.as_ref(), &plaintext);
|
|
142
|
+
let geometry = window_geometry(plan, replay.as_ref(), &plaintext);
|
|
144
143
|
Ok(ProbeSubmission {
|
|
145
144
|
payload: Submission {
|
|
146
145
|
plaintext,
|
|
@@ -152,33 +151,28 @@ pub(crate) async fn prepare(session: &WidgetSession<'_>, solve: &Solve) -> Resul
|
|
|
152
151
|
})
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
/// Collects
|
|
156
|
-
///
|
|
154
|
+
/// Collects the measurements that make no requests, except the stack. Probes with requests make
|
|
155
|
+
/// them as they run; see [`run_probes`].
|
|
157
156
|
async fn observe(
|
|
158
157
|
session: &WidgetSession<'_>,
|
|
159
158
|
plan: &ProbePlan,
|
|
160
|
-
options: &Options,
|
|
161
159
|
profile: &Profile,
|
|
162
|
-
) -> Result<
|
|
160
|
+
) -> Result<Observed> {
|
|
163
161
|
let mut observed = Observed::default();
|
|
164
|
-
let requests = requests(session, plan, options, profile, &mut observed);
|
|
165
|
-
let stack = probe_stack(session, plan);
|
|
166
|
-
let (fetches, stack) = tokio::try_join!(requests, stack)?;
|
|
167
|
-
observed.stack = stack;
|
|
168
162
|
let facts = &profile.facts;
|
|
169
163
|
let resolution = session.clock_resolution();
|
|
170
|
-
if let Some((probe, Operation::Benchmark { fields })) = plan.operation(Kind::Benchmark) {
|
|
171
|
-
let measured = fields
|
|
172
|
-
.measure(&facts.benchmark_worker, resolution)
|
|
173
|
-
.with_context(|| format!("running probe {}'s benchmark", probe.index))?;
|
|
174
|
-
observed.benchmark = Some(measured);
|
|
175
|
-
}
|
|
176
164
|
if let (Some((probe, _)), Some(previous)) = (plan.operation(Kind::Media), &facts.media) {
|
|
177
165
|
let media = ice::refresh(previous.clone())
|
|
178
166
|
.with_context(|| format!("refreshing probe {}'s media", probe.index))?;
|
|
179
167
|
session.sink.json("media-measurement.json", &media)?;
|
|
180
168
|
observed.media = Some(media);
|
|
181
169
|
}
|
|
170
|
+
if let Some((probe, Operation::Benchmark { fields })) = plan.operation(Kind::Benchmark) {
|
|
171
|
+
let measured = fields
|
|
172
|
+
.measure(&facts.benchmark_worker, resolution)
|
|
173
|
+
.with_context(|| format!("running probe {}'s benchmark", probe.index))?;
|
|
174
|
+
observed.benchmark = Some(measured);
|
|
175
|
+
}
|
|
182
176
|
// The worker compares 5000 consecutive clock-reading pairs. Chrome's call overhead usually
|
|
183
177
|
// crosses a 100 µs boundary; faster native reads may miss all boundaries. Retry until a
|
|
184
178
|
// pair crosses one.
|
|
@@ -190,7 +184,7 @@ async fn observe(
|
|
|
190
184
|
.unwrap_or_else(|| worker.back_to_back(clock::PAIRS));
|
|
191
185
|
observed.clock = Some(readings);
|
|
192
186
|
}
|
|
193
|
-
Ok(
|
|
187
|
+
Ok(observed)
|
|
194
188
|
}
|
|
195
189
|
|
|
196
190
|
/// Runs the WebSocket echo exchange. After opening, sends `String(performance.now())` and sends a
|
|
@@ -278,7 +272,8 @@ async fn probe_stack(session: &WidgetSession<'_>, plan: &ProbePlan) -> Result<Op
|
|
|
278
272
|
let origin = fetched.script_position()?;
|
|
279
273
|
let engine = session.client.engine();
|
|
280
274
|
let stack = task::spawn_blocking(move || {
|
|
281
|
-
let
|
|
275
|
+
let resumed = call_stack::Resumption::WorkerMessage;
|
|
276
|
+
let geometry = call_stack::recover_probe(&javascript, &cases, calls, resumed)?;
|
|
282
277
|
anyhow::Ok(geometry.format(&origin, engine))
|
|
283
278
|
})
|
|
284
279
|
.await
|
|
@@ -310,109 +305,145 @@ fn durations(
|
|
|
310
305
|
plan.probes().iter().map(duration).collect()
|
|
311
306
|
}
|
|
312
307
|
|
|
313
|
-
///
|
|
314
|
-
|
|
308
|
+
/// Runs the probes in order from the end of startup, or now if later. A probe that makes
|
|
309
|
+
/// requests waits for its start and lasts at least until they end. Returns the probes' marks
|
|
310
|
+
/// and the loads resource timing lists.
|
|
311
|
+
async fn run_probes(
|
|
315
312
|
session: &WidgetSession<'_>,
|
|
316
313
|
plan: &ProbePlan,
|
|
317
314
|
options: &Options,
|
|
318
315
|
profile: &Profile,
|
|
319
316
|
observed: &mut Observed,
|
|
320
|
-
|
|
321
|
-
|
|
317
|
+
planned: &[i64],
|
|
318
|
+
) -> Result<(Schedule, Vec<LoadTiming>)> {
|
|
319
|
+
let startup_end = session.arrived_ms + i64::from(profile.startup_ms);
|
|
320
|
+
let mut timeline = Timeline::starting(performance_clock::epoch_ms().max(startup_end));
|
|
322
321
|
let mut fetches = Vec::new();
|
|
323
|
-
for probe in plan.probes() {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
_ => host_reachability::Outcome::Rejected,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
observed.host_reachability = Some(outcomes);
|
|
378
|
-
}
|
|
379
|
-
Some(Operation::WebSocketEcho { fields }) => {
|
|
380
|
-
observed.websocket_echo = Some(echo_websocket(session, fields, options).await?);
|
|
322
|
+
for (probe, &planned_ms) in plan.probes().iter().zip(planned) {
|
|
323
|
+
let requests = matches!(
|
|
324
|
+
probe.operation,
|
|
325
|
+
Some(
|
|
326
|
+
Operation::PrivateToken { .. }
|
|
327
|
+
| Operation::WorkerFetch { .. }
|
|
328
|
+
| Operation::HostReachability { .. }
|
|
329
|
+
| Operation::WebSocketEcho { .. }
|
|
330
|
+
| Operation::Image { .. }
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
let finished = if requests {
|
|
334
|
+
timing::sleep_until(timeline.next_start).await;
|
|
335
|
+
request(session, probe, options, profile, observed, &mut fetches).await?;
|
|
336
|
+
Some(performance_clock::epoch_ms())
|
|
337
|
+
} else {
|
|
338
|
+
None
|
|
339
|
+
};
|
|
340
|
+
timeline.finish(planned_ms, finished);
|
|
341
|
+
}
|
|
342
|
+
Ok((timeline.schedule(profile.pending_ms.into()), fetches))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Makes `probe`'s requests as the iframe would. Records its measurement in `observed` and the
|
|
346
|
+
/// loads resource timing lists in `fetches`.
|
|
347
|
+
async fn request(
|
|
348
|
+
session: &WidgetSession<'_>,
|
|
349
|
+
probe: &Probe,
|
|
350
|
+
options: &Options,
|
|
351
|
+
profile: &Profile,
|
|
352
|
+
observed: &mut Observed,
|
|
353
|
+
fetches: &mut Vec<LoadTiming>,
|
|
354
|
+
) -> Result<()> {
|
|
355
|
+
let (client, iframe) = (session.client, &session.fetched.iframe);
|
|
356
|
+
match &probe.operation {
|
|
357
|
+
Some(Operation::PrivateToken { fields }) => {
|
|
358
|
+
let url = fields.url(options, &probe.input, &iframe.url)?;
|
|
359
|
+
let started_ms = performance_clock::epoch_ms();
|
|
360
|
+
// The worker calls `r.text()` before posting results. Treat body-read failures
|
|
361
|
+
// like fetch failures because both reject the promise chain.
|
|
362
|
+
let fetched = http::fetch_from_worker(client, iframe, &url, FetchOptions::NoCache);
|
|
363
|
+
let outcome = async {
|
|
364
|
+
let response = fetched.await?;
|
|
365
|
+
let status = response.status();
|
|
366
|
+
anyhow::Ok(private_token::WorkerOutcome::Response {
|
|
367
|
+
ok: status.is_success(),
|
|
368
|
+
status: status.as_u16(),
|
|
369
|
+
text: response.text().await?,
|
|
370
|
+
})
|
|
381
371
|
}
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
.await
|
|
373
|
+
.unwrap_or_else(|error| private_token::WorkerOutcome::Rejected {
|
|
374
|
+
worker_error: error.to_string(),
|
|
375
|
+
});
|
|
376
|
+
observed.private_token = Some(private_token::Measurement {
|
|
377
|
+
outcome,
|
|
378
|
+
started_ms,
|
|
379
|
+
finished_ms: performance_clock::epoch_ms(),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
Some(Operation::WorkerFetch { .. }) if profile.facts.worker_fetch_rejected => {
|
|
383
|
+
observed.worker_fetch = Some(worker_fetch::Measurement::Rejected);
|
|
384
|
+
}
|
|
385
|
+
Some(Operation::HostReachability { fields }) => {
|
|
386
|
+
// CORS rejects a response that does not allow the iframe's origin.
|
|
387
|
+
let origin = iframe.url.origin().ascii_serialization();
|
|
388
|
+
let mut outcomes = Vec::new();
|
|
389
|
+
for url in &fields.urls {
|
|
390
|
+
let url = Url::parse(url).context("parsing a host reachability URL")?;
|
|
391
|
+
let timer = Timer::start();
|
|
384
392
|
let options = FetchOptions::NoStoreWithoutCredentials;
|
|
385
|
-
let fetched = http::fetch_from_worker(client, iframe, &url, options)
|
|
386
|
-
|
|
387
|
-
Ok(response)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.
|
|
391
|
-
.
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
let fetched = http::fetch_from_worker(client, iframe, &url, options);
|
|
394
|
+
outcomes.push(match time::timeout(REACHABILITY_TIMEOUT, fetched).await {
|
|
395
|
+
Ok(Ok(response))
|
|
396
|
+
if response
|
|
397
|
+
.headers()
|
|
398
|
+
.get("access-control-allow-origin")
|
|
399
|
+
.and_then(|allowed| allowed.to_str().ok())
|
|
400
|
+
.is_some_and(|allowed| allowed == "*" || allowed == origin) =>
|
|
401
|
+
{
|
|
402
|
+
host_reachability::Outcome::Answered {
|
|
403
|
+
elapsed_ms: timer.elapsed_ms() as i64,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
_ => host_reachability::Outcome::Rejected,
|
|
394
407
|
});
|
|
395
408
|
}
|
|
396
|
-
Some(
|
|
397
|
-
let url = fields.url(options, &probe.input, &iframe.url)?;
|
|
398
|
-
let timer = Timer::start();
|
|
399
|
-
let response = http::fetch_image(client, iframe, &url).await?;
|
|
400
|
-
let waiting_ms = timer.elapsed_ms();
|
|
401
|
-
let ok = response.status().is_success();
|
|
402
|
-
let (body, network) = http::read_encoded(response)
|
|
403
|
-
.await
|
|
404
|
-
.context("reading a challenge image")?;
|
|
405
|
-
fetches.push(timer.phased(url.as_str(), waiting_ms, network.encoded_bytes));
|
|
406
|
-
observed.image = Some(image::Measurement::loaded(
|
|
407
|
-
ok.then_some(&body[..]),
|
|
408
|
-
&profile.image.effective_type,
|
|
409
|
-
profile.image.online,
|
|
410
|
-
));
|
|
411
|
-
}
|
|
412
|
-
_ => {}
|
|
409
|
+
observed.host_reachability = Some(outcomes);
|
|
413
410
|
}
|
|
411
|
+
Some(Operation::WebSocketEcho { fields }) => {
|
|
412
|
+
observed.websocket_echo = Some(echo_websocket(session, fields, options).await?);
|
|
413
|
+
}
|
|
414
|
+
Some(Operation::WorkerFetch { fields }) => {
|
|
415
|
+
let url = fields.url(options)?;
|
|
416
|
+
let options = FetchOptions::NoStoreWithoutCredentials;
|
|
417
|
+
let fetched = http::fetch_from_worker(client, iframe, &url, options).await;
|
|
418
|
+
observed.worker_fetch = Some(match fetched {
|
|
419
|
+
Ok(response) => worker_fetch::Measurement::Response {
|
|
420
|
+
status: response.status().as_u16(),
|
|
421
|
+
headers: http::text_headers(response.headers())
|
|
422
|
+
.map(|(name, value)| (name.to_owned(), value))
|
|
423
|
+
.collect(),
|
|
424
|
+
},
|
|
425
|
+
Err(_) => worker_fetch::Measurement::Rejected,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
Some(Operation::Image { fields }) => {
|
|
429
|
+
let url = fields.url(options, &probe.input, &iframe.url)?;
|
|
430
|
+
let timer = Timer::start();
|
|
431
|
+
let response = http::fetch_image(client, iframe, &url).await?;
|
|
432
|
+
let waiting_ms = timer.elapsed_ms();
|
|
433
|
+
let ok = response.status().is_success();
|
|
434
|
+
let (body, network) = http::read_encoded(response)
|
|
435
|
+
.await
|
|
436
|
+
.context("reading a challenge image")?;
|
|
437
|
+
fetches.push(timer.phased(url.as_str(), waiting_ms, network.encoded_bytes));
|
|
438
|
+
observed.image = Some(image::Measurement::loaded(
|
|
439
|
+
ok.then_some(&body[..]),
|
|
440
|
+
&profile.image.effective_type,
|
|
441
|
+
profile.image.online,
|
|
442
|
+
));
|
|
443
|
+
}
|
|
444
|
+
_ => {}
|
|
414
445
|
}
|
|
415
|
-
Ok(
|
|
446
|
+
Ok(())
|
|
416
447
|
}
|
|
417
448
|
|
|
418
449
|
/// Builds probe records with scheduled timestamps. Stored answers use `stored` and preserve the
|
|
@@ -464,9 +495,13 @@ fn records(
|
|
|
464
495
|
}
|
|
465
496
|
|
|
466
497
|
/// Window dimensions and position already reported by the environment audit.
|
|
467
|
-
fn window_geometry(
|
|
498
|
+
fn window_geometry(
|
|
499
|
+
plan: &ProbePlan,
|
|
500
|
+
replay: Option<&Replay<'_>>,
|
|
501
|
+
payload: &Payload,
|
|
502
|
+
) -> BTreeMap<String, f64> {
|
|
468
503
|
let groups = replay
|
|
469
|
-
.and_then(|replay| replay.audit_groups(payload))
|
|
504
|
+
.and_then(|replay| replay.audit_groups(plan, payload))
|
|
470
505
|
.into_iter()
|
|
471
506
|
.flatten();
|
|
472
507
|
let mut geometry = BTreeMap::new();
|
|
@@ -79,32 +79,43 @@ pub(crate) struct Schedule {
|
|
|
79
79
|
pub send_at: i64,
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
///
|
|
83
|
-
///
|
|
84
|
-
|
|
85
|
-
///
|
|
86
|
-
pub
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
) ->
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
start
|
|
82
|
+
/// Probe marks laid out as the probes run: in order, one millisecond apart. A probe lasts its
|
|
83
|
+
/// planned duration, or until the work it waits for, such as a request, ends.
|
|
84
|
+
pub(crate) struct Timeline {
|
|
85
|
+
/// When the next probe starts, in epoch milliseconds.
|
|
86
|
+
pub next_start: i64,
|
|
87
|
+
marks: Vec<ProbeMarks>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Timeline {
|
|
91
|
+
#[must_use]
|
|
92
|
+
pub(crate) fn starting(next_start: i64) -> Self {
|
|
93
|
+
Self {
|
|
94
|
+
next_start,
|
|
95
|
+
marks: Vec::new(),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Ends the next probe `planned_ms` after it starts, or at `finished_ms` if its work ended
|
|
100
|
+
/// later.
|
|
101
|
+
pub(crate) fn finish(&mut self, planned_ms: i64, finished_ms: Option<i64>) {
|
|
102
|
+
let start = self.next_start;
|
|
103
|
+
let planned_end = start + planned_ms;
|
|
104
|
+
let end = finished_ms.map_or(planned_end, |finished| finished.max(planned_end));
|
|
105
|
+
self.marks.push(ProbeMarks { start, end });
|
|
106
|
+
self.next_start = end + 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Sends `pending_ms` after the last probe's next mark. Programs without probes send when
|
|
110
|
+
/// they start.
|
|
111
|
+
#[must_use]
|
|
112
|
+
pub(crate) fn schedule(self, pending_ms: i64) -> Schedule {
|
|
113
|
+
let pending_ms = if self.marks.is_empty() { 0 } else { pending_ms };
|
|
114
|
+
Schedule {
|
|
115
|
+
marks: self.marks,
|
|
116
|
+
send_at: self.next_start + pending_ms,
|
|
117
|
+
}
|
|
106
118
|
}
|
|
107
|
-
Schedule { marks, send_at }
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
#[cfg(test)]
|
|
@@ -112,13 +123,14 @@ mod tests {
|
|
|
112
123
|
use super::*;
|
|
113
124
|
|
|
114
125
|
#[test]
|
|
115
|
-
fn
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
126
|
+
fn probes_run_in_order_and_last_until_their_requests_end() {
|
|
127
|
+
let mut timeline = Timeline::starting(1_400);
|
|
128
|
+
timeline.finish(3, None);
|
|
129
|
+
// The request took longer than planned.
|
|
130
|
+
timeline.finish(5, Some(1_430));
|
|
131
|
+
// The request ended before the planned duration ran out.
|
|
132
|
+
timeline.finish(10, Some(1_433));
|
|
133
|
+
let schedule = timeline.schedule(15);
|
|
122
134
|
assert_eq!(
|
|
123
135
|
schedule.marks,
|
|
124
136
|
[
|
|
@@ -128,35 +140,28 @@ mod tests {
|
|
|
128
140
|
},
|
|
129
141
|
ProbeMarks {
|
|
130
142
|
start: 1_404,
|
|
131
|
-
end:
|
|
143
|
+
end: 1_430
|
|
132
144
|
},
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
ProbeMarks {
|
|
146
|
+
start: 1_431,
|
|
147
|
+
end: 1_441
|
|
148
|
+
},
|
|
149
|
+
]
|
|
135
150
|
);
|
|
136
|
-
|
|
137
|
-
let late = super::schedule(&[3, 5], 400, 15, 1_000, 2_000);
|
|
138
|
-
assert_eq!(late.send_at, 2_000);
|
|
139
151
|
assert_eq!(
|
|
140
|
-
|
|
141
|
-
"
|
|
152
|
+
schedule.send_at, 1_457,
|
|
153
|
+
"the last probe's next mark is exactly `pending_ms` before sending"
|
|
142
154
|
);
|
|
155
|
+
assert_eq!(Timeline::starting(900).schedule(15).send_at, 900);
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
#[test]
|
|
146
|
-
fn
|
|
147
|
-
let
|
|
159
|
+
fn signed_durations_are_kept() {
|
|
160
|
+
let mut timeline = Timeline::starting(0);
|
|
161
|
+
timeline.finish(-2, None);
|
|
148
162
|
assert_eq!(
|
|
149
|
-
schedule,
|
|
150
|
-
|
|
151
|
-
marks: Vec::new(),
|
|
152
|
-
send_at: 1_400
|
|
153
|
-
}
|
|
163
|
+
timeline.schedule(0).marks,
|
|
164
|
+
[ProbeMarks { start: 0, end: -2 }]
|
|
154
165
|
);
|
|
155
166
|
}
|
|
156
|
-
|
|
157
|
-
#[test]
|
|
158
|
-
fn signed_durations_are_kept() {
|
|
159
|
-
let schedule = schedule(&[-2, 4], 0, 0, 0, 0);
|
|
160
|
-
assert_eq!(schedule.marks[0].end - schedule.marks[0].start, -2);
|
|
161
|
-
}
|
|
162
167
|
}
|
|
@@ -11,7 +11,6 @@ use super::initial::{self, EmbeddingPage, Fetched, Merged};
|
|
|
11
11
|
use super::timing::Timer;
|
|
12
12
|
use super::{Sink, interaction, probes};
|
|
13
13
|
use crate::browser::{Page, Solve};
|
|
14
|
-
use crate::challenge::installed::Installed;
|
|
15
14
|
use crate::challenge::orchestrator::ChallengePage;
|
|
16
15
|
use crate::challenge::program::{ExtractedProgram, PreparedSession, SessionEncoder};
|
|
17
16
|
use crate::challenge::source::InitialRequest;
|
|
@@ -128,7 +127,7 @@ enum Landing {
|
|
|
128
127
|
/// A site's page with a Turnstile widget.
|
|
129
128
|
Site(EmbeddingPage),
|
|
130
129
|
/// A challenge page, whose orchestrator has rendered its widget.
|
|
131
|
-
ChallengePage(OrchestratorSession
|
|
130
|
+
ChallengePage(Box<OrchestratorSession>, EmbeddingPage),
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
impl Landing {
|
|
@@ -177,7 +176,10 @@ async fn land(
|
|
|
177
176
|
document.referrer_policy,
|
|
178
177
|
)
|
|
179
178
|
.await?;
|
|
180
|
-
Ok(Landing::ChallengePage(
|
|
179
|
+
Ok(Landing::ChallengePage(
|
|
180
|
+
Box::new(orchestrator),
|
|
181
|
+
embedding_page,
|
|
182
|
+
))
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
/// Starts the session of the widget rendered by `embedding_page` by sending its first payload.
|
|
@@ -212,7 +214,6 @@ async fn start_widget<'caller>(
|
|
|
212
214
|
sink,
|
|
213
215
|
fetched,
|
|
214
216
|
request: program.request,
|
|
215
|
-
installed: program.installed,
|
|
216
217
|
encoder,
|
|
217
218
|
embedding: embedding_page.embedding,
|
|
218
219
|
merged,
|
|
@@ -316,8 +317,6 @@ pub(crate) struct WidgetSession<'caller> {
|
|
|
316
317
|
pub fetched: Fetched,
|
|
317
318
|
/// POST destination and options reported by the first payload.
|
|
318
319
|
pub request: InitialRequest,
|
|
319
|
-
/// Initial-program facts reused when refreshing captured probe answers.
|
|
320
|
-
pub installed: Installed,
|
|
321
320
|
pub encoder: SessionEncoder,
|
|
322
321
|
pub embedding: Embedding,
|
|
323
322
|
pub merged: Merged,
|