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
|
@@ -9,27 +9,26 @@
|
|
|
9
9
|
//! plus trace membership and navigation sizes in the environment audit. Live requests, fresh
|
|
10
10
|
//! WebRTC state, benchmark results, and stacks are computed separately.
|
|
11
11
|
|
|
12
|
-
use std::collections::
|
|
12
|
+
use std::collections::BTreeMap;
|
|
13
13
|
use std::mem;
|
|
14
14
|
use std::ops::Range;
|
|
15
15
|
|
|
16
16
|
use anyhow::{Context, Result, ensure};
|
|
17
17
|
use serde::{Deserialize, Serialize};
|
|
18
18
|
|
|
19
|
-
use super::
|
|
20
|
-
|
|
21
|
-
use super::probes::{stylesheets, worker_navigator};
|
|
19
|
+
pub use super::audit_globals::{Globals, Signature};
|
|
20
|
+
use super::probes::worker_navigator;
|
|
22
21
|
use super::records::{ArrangedFields, Probe, ProbePlan};
|
|
23
22
|
use super::reference::{
|
|
24
23
|
ENVIRONMENT_AUDIT_DECODED, ENVIRONMENT_AUDIT_ENCODED, ENVIRONMENT_AUDIT_GROUPS,
|
|
25
24
|
ENVIRONMENT_AUDIT_REDUCTION, ENVIRONMENT_AUDIT_VALUES, ENVIRONMENT_REFERRER, REFERENCE,
|
|
26
|
-
WORKER_NAVIGATOR_RESULT,
|
|
25
|
+
STYLESHEETS_HASH, WORKER_NAVIGATOR_RESULT,
|
|
27
26
|
};
|
|
28
27
|
use super::registry::{AUDIT_PROBE, Kind, Operation, Treatment};
|
|
29
|
-
pub use super::
|
|
28
|
+
pub use super::stylesheets::Stylesheet;
|
|
30
29
|
use super::trace::Trace;
|
|
31
|
-
use super::vocabulary::Vocabulary;
|
|
32
|
-
use
|
|
30
|
+
use super::vocabulary::{self, Vocabulary};
|
|
31
|
+
use super::{analysis, stylesheets};
|
|
33
32
|
use crate::http::ResponseMetrics;
|
|
34
33
|
use crate::js::{JsObject, JsValue, Options, Payload, json};
|
|
35
34
|
use crate::script::Source;
|
|
@@ -43,12 +42,12 @@ pub struct Answers {
|
|
|
43
42
|
pub probes: BTreeMap<usize, Stored>,
|
|
44
43
|
/// The worker navigator, by API property.
|
|
45
44
|
pub navigator: JsObject,
|
|
46
|
-
///
|
|
45
|
+
/// Definitions of the captured iframe globals, under the audit's stored names.
|
|
46
|
+
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
|
47
|
+
pub globals: BTreeMap<String, Signature>,
|
|
48
|
+
/// CSSOM measurements, checked against the current CSS before reuse.
|
|
47
49
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
48
|
-
pub stylesheets: Vec<
|
|
49
|
-
/// Source evidence for names that change between sessions.
|
|
50
|
-
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
51
|
-
pub names: Option<CapturedNames>,
|
|
50
|
+
pub stylesheets: Vec<Stylesheet>,
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/// One probe's stored record, without its envelope.
|
|
@@ -65,17 +64,52 @@ pub struct Stored {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
impl Answers {
|
|
67
|
+
/// Attaches browser CSSOM measurements only after reproducing this capture's reported hash.
|
|
68
|
+
pub fn measure_stylesheets(
|
|
69
|
+
&mut self,
|
|
70
|
+
source: &Source,
|
|
71
|
+
program: &[u8],
|
|
72
|
+
html: &str,
|
|
73
|
+
templates: Vec<Stylesheet>,
|
|
74
|
+
) -> Result<()> {
|
|
75
|
+
let analysis = analysis::analyze(source, program)?;
|
|
76
|
+
let (probe, excluded) = analysis
|
|
77
|
+
.plan()
|
|
78
|
+
.probes()
|
|
79
|
+
.iter()
|
|
80
|
+
.find_map(|probe| Some((probe, stylesheets::excluded_selector(probe)?)))
|
|
81
|
+
.context("the capture has no stylesheet probe")?;
|
|
82
|
+
let excluded = excluded?;
|
|
83
|
+
let partner = analysis
|
|
84
|
+
.plan()
|
|
85
|
+
.vocabulary()
|
|
86
|
+
.partner(probe.position())
|
|
87
|
+
.context("the stylesheet probe has no reference partner")?;
|
|
88
|
+
let stored = self
|
|
89
|
+
.probes
|
|
90
|
+
.get(&partner)
|
|
91
|
+
.context("no stored stylesheet answer")?;
|
|
92
|
+
let mut fields = JsObject::new();
|
|
93
|
+
stylesheets::patch(html, &templates, excluded, &mut fields)?;
|
|
94
|
+
ensure!(
|
|
95
|
+
fields.get(STYLESHEETS_HASH) == stored.fields.get(STYLESHEETS_HASH),
|
|
96
|
+
"CSSOM measurements do not reproduce the captured stylesheet hash"
|
|
97
|
+
);
|
|
98
|
+
self.stylesheets = templates;
|
|
99
|
+
Ok(())
|
|
100
|
+
}
|
|
101
|
+
|
|
68
102
|
/// Builds stored answers from a captured iframe script, probe program, and second payload.
|
|
69
103
|
pub fn capture(source: &Source, program: &[u8], payload: &Payload) -> Result<Self> {
|
|
70
104
|
let analysis = analysis::analyze(source, program)?;
|
|
71
105
|
let plan = analysis.plan();
|
|
72
106
|
let vocabulary = plan.vocabulary();
|
|
107
|
+
let globals = Globals::capture(source)?;
|
|
73
108
|
let envelope = &plan.record_fields;
|
|
74
109
|
let envelope_names = envelope.names();
|
|
75
110
|
let is_envelope = |name: &String| envelope_names.contains(&name.as_str());
|
|
76
111
|
let mut answers = Self {
|
|
77
112
|
reference: REFERENCE.digest.clone(),
|
|
78
|
-
names: Some(CapturedNames::capture(source, plan)?),
|
|
79
113
|
..Self::default()
|
|
80
114
|
};
|
|
81
115
|
for probe in plan.probes() {
|
|
@@ -125,6 +159,10 @@ impl Answers {
|
|
|
125
159
|
vocabulary.rewrite_to_reference(position, &mut fields);
|
|
126
160
|
// Replay refreshes an audit's navigation sizes and trace by reference name.
|
|
127
161
|
if operation.family() == Kind::EnvironmentAudit {
|
|
162
|
+
for (name, signature) in &globals.0 {
|
|
163
|
+
let name = vocabulary.to_reference(position, name).to_owned();
|
|
164
|
+
answers.globals.insert(name, signature.clone());
|
|
165
|
+
}
|
|
128
166
|
ensure!(
|
|
129
167
|
fields.contains_key(ENVIRONMENT_AUDIT_GROUPS)
|
|
130
168
|
&& fields.contains_key(ENVIRONMENT_AUDIT_VALUES),
|
|
@@ -158,144 +196,33 @@ impl Answers {
|
|
|
158
196
|
/// Stored answers matched to one session's probes.
|
|
159
197
|
pub struct Replay<'answers> {
|
|
160
198
|
answers: &'answers Answers,
|
|
161
|
-
vocabulary: Vocabulary,
|
|
162
|
-
refresh: Option<super::session_names::Refresh>,
|
|
163
|
-
plan: &'answers ProbePlan,
|
|
199
|
+
vocabulary: &'answers Vocabulary,
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
impl<'answers> Replay<'answers> {
|
|
167
|
-
/// Matches
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
source: &Source,
|
|
172
|
-
installed: &Installed,
|
|
173
|
-
) -> Result<Self> {
|
|
174
|
-
answers.check()?;
|
|
175
|
-
let mut vocabulary = plan.vocabulary().clone();
|
|
176
|
-
let mut common: HashSet<&str> = plan
|
|
177
|
-
.probes()
|
|
178
|
-
.first()
|
|
179
|
-
.into_iter()
|
|
180
|
-
.flat_map(|probe| probe.features.iter().map(String::as_str))
|
|
181
|
-
.collect();
|
|
182
|
-
for probe in plan.probes() {
|
|
183
|
-
common.retain(|name| probe.features.iter().any(|text| text == name));
|
|
184
|
-
}
|
|
185
|
-
for probe in plan.probes() {
|
|
186
|
-
if let Some(partner) = vocabulary.partner(probe.position())
|
|
187
|
-
&& let Some(stored) = answers.probes.get(&partner)
|
|
188
|
-
{
|
|
189
|
-
let wanted: Vec<String> = stored.fields.keys().cloned().collect();
|
|
190
|
-
vocabulary.refine_outputs(
|
|
191
|
-
probe.position(),
|
|
192
|
-
&probe.features,
|
|
193
|
-
&probe.writes.fields,
|
|
194
|
-
&probe.writes.internal,
|
|
195
|
-
&common,
|
|
196
|
-
&wanted,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
let refresh = answers
|
|
201
|
-
.names
|
|
202
|
-
.as_ref()
|
|
203
|
-
.map(|names| super::session_names::Refresh::new(names, source, plan, installed))
|
|
204
|
-
.transpose()?;
|
|
205
|
-
let replay = Self {
|
|
203
|
+
/// Matches `answers` to `plan`'s probes using the plan's vocabulary.
|
|
204
|
+
#[must_use]
|
|
205
|
+
pub fn new(answers: &'answers Answers, plan: &'answers ProbePlan) -> Self {
|
|
206
|
+
Self {
|
|
206
207
|
answers,
|
|
207
|
-
vocabulary,
|
|
208
|
-
refresh,
|
|
209
|
-
plan,
|
|
210
|
-
};
|
|
211
|
-
replay.validate()?;
|
|
212
|
-
Ok(replay)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fn validate(&self) -> Result<()> {
|
|
216
|
-
let mut failures = Vec::new();
|
|
217
|
-
for probe in self.plan.probes() {
|
|
218
|
-
let Some(partner) = self.vocabulary.partner(probe.position()) else {
|
|
219
|
-
continue;
|
|
220
|
-
};
|
|
221
|
-
let Some(stored) = self.answers.probes.get(&partner) else {
|
|
222
|
-
continue;
|
|
223
|
-
};
|
|
224
|
-
if probe.writes.fields.is_empty() {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
let missing: Vec<&String> =
|
|
228
|
-
stored
|
|
229
|
-
.fields
|
|
230
|
-
.keys()
|
|
231
|
-
.filter(|name| {
|
|
232
|
-
!probe.writes.fields.iter().any(|field| {
|
|
233
|
-
field == self.vocabulary.to_session(probe.position(), name)
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
.collect();
|
|
237
|
-
if !missing.is_empty() {
|
|
238
|
-
failures.push(format!("probe {}: {:?}", probe.index, missing));
|
|
239
|
-
}
|
|
240
|
-
if probe
|
|
241
|
-
.expanded_features
|
|
242
|
-
.iter()
|
|
243
|
-
.any(|name| super::vocabulary::hashed_name(name))
|
|
244
|
-
{
|
|
245
|
-
let mut names = BTreeSet::new();
|
|
246
|
-
fn collect<'a>(value: &'a JsValue, names: &mut BTreeSet<&'a str>) {
|
|
247
|
-
match value {
|
|
248
|
-
JsValue::String(text) => {
|
|
249
|
-
names.insert(text);
|
|
250
|
-
}
|
|
251
|
-
JsValue::Object(object) => {
|
|
252
|
-
for (key, value) in object {
|
|
253
|
-
names.insert(key);
|
|
254
|
-
collect(value, names);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
JsValue::Array(array) => {
|
|
258
|
-
for value in array {
|
|
259
|
-
collect(value, names);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
_ => {}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
for (key, value) in &stored.fields {
|
|
266
|
-
names.insert(key.as_str());
|
|
267
|
-
collect(value, &mut names);
|
|
268
|
-
}
|
|
269
|
-
let reference = &super::reference::EXPANDED_REFERENCE.probes[partner - 1];
|
|
270
|
-
names.retain(|name| {
|
|
271
|
-
super::vocabulary::is_name(name)
|
|
272
|
-
&& reference.iter().any(|text| text == name)
|
|
273
|
-
&& !probe.expanded_features.iter().any(|text| text == name)
|
|
274
|
-
&& self.vocabulary.to_session(probe.position(), name) == *name
|
|
275
|
-
});
|
|
276
|
-
if !names.is_empty() {
|
|
277
|
-
failures.push(format!("probe {} nested values: {:?}", probe.index, names));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
208
|
+
vocabulary: plan.vocabulary(),
|
|
280
209
|
}
|
|
281
|
-
ensure!(
|
|
282
|
-
failures.is_empty(),
|
|
283
|
-
"unmapped stored result fields: {}",
|
|
284
|
-
failures.join("; ")
|
|
285
|
-
);
|
|
286
|
-
Ok(())
|
|
287
210
|
}
|
|
288
211
|
|
|
289
212
|
/// Finds environment-audit groups that report window dimensions and position.
|
|
290
213
|
#[must_use]
|
|
291
|
-
pub fn audit_groups<'r>(
|
|
292
|
-
|
|
214
|
+
pub fn audit_groups<'r>(
|
|
215
|
+
&self,
|
|
216
|
+
plan: &ProbePlan,
|
|
217
|
+
records: &'r JsObject,
|
|
218
|
+
) -> Option<&'r JsObject> {
|
|
219
|
+
let position = (0..plan.probes().len())
|
|
293
220
|
.find(|&position| self.vocabulary.partner(position) == Some(AUDIT_PROBE))?;
|
|
294
221
|
let groups = self
|
|
295
222
|
.vocabulary
|
|
296
223
|
.to_session(position, ENVIRONMENT_AUDIT_GROUPS);
|
|
297
224
|
records
|
|
298
|
-
.get(&
|
|
225
|
+
.get(&plan.probes()[position].index.to_string())?
|
|
299
226
|
.get(groups)?
|
|
300
227
|
.as_object()
|
|
301
228
|
}
|
|
@@ -330,33 +257,20 @@ impl<'answers> Replay<'answers> {
|
|
|
330
257
|
let partner = self.vocabulary.partner(position).with_context(missing)?;
|
|
331
258
|
let stored = self.answers.probes.get(&partner).with_context(missing)?;
|
|
332
259
|
let mut fields = stored.fields.clone();
|
|
333
|
-
|
|
334
|
-
if
|
|
260
|
+
let measured = &self.answers.stylesheets;
|
|
261
|
+
if !measured.is_empty()
|
|
262
|
+
&& let Some(excluded) = stylesheets::excluded_selector(probe)
|
|
335
263
|
{
|
|
336
|
-
stylesheets::
|
|
337
|
-
&self.answers.stylesheets,
|
|
338
|
-
probe,
|
|
339
|
-
session.iframe_html,
|
|
340
|
-
&mut fields,
|
|
341
|
-
)?;
|
|
342
|
-
}
|
|
343
|
-
// Profiles without source evidence retain the original trace-only refresh.
|
|
344
|
-
if self.refresh.is_none()
|
|
345
|
-
&& let Some(groups) = fields
|
|
346
|
-
.get_mut(ENVIRONMENT_AUDIT_GROUPS)
|
|
347
|
-
.and_then(JsValue::as_object_mut)
|
|
348
|
-
{
|
|
349
|
-
let trace = session
|
|
350
|
-
.trace
|
|
351
|
-
.as_ref()
|
|
352
|
-
.context("the environment audit needs the iframe's trace")?;
|
|
353
|
-
refresh_trace(trace, groups, &probe.features, session.controller)?;
|
|
264
|
+
stylesheets::patch(session.iframe_html, measured, excluded?, &mut fields)?;
|
|
354
265
|
}
|
|
266
|
+
session.patch(probe, &mut fields, &self.answers.globals)?;
|
|
355
267
|
let mut arranged = ArrangedFields::split(fields, &stored.late);
|
|
356
268
|
for fields in [&mut arranged.early, &mut arranged.late] {
|
|
357
269
|
self.vocabulary.rewrite_to_session(position, fields);
|
|
358
|
-
|
|
359
|
-
|
|
270
|
+
for (name, value) in &probe.replay_literals {
|
|
271
|
+
if let Some(field) = fields.get_mut(name) {
|
|
272
|
+
*field = value.clone();
|
|
273
|
+
}
|
|
360
274
|
}
|
|
361
275
|
}
|
|
362
276
|
Ok(arranged)
|
|
@@ -365,10 +279,10 @@ impl<'answers> Replay<'answers> {
|
|
|
365
279
|
|
|
366
280
|
/// Current-session values needed to refresh stored records.
|
|
367
281
|
pub struct Session<'session> {
|
|
282
|
+
/// Current iframe HTML, including its inline styles.
|
|
283
|
+
pub iframe_html: &'session str,
|
|
368
284
|
/// The iframe's URL, which records report as the referrer, base URI and stack locations.
|
|
369
285
|
pub iframe_url: &'session str,
|
|
370
|
-
/// Current iframe stylesheets, used to refresh captured CSSOM measurements.
|
|
371
|
-
pub iframe_html: &'session str,
|
|
372
286
|
/// `document.lastModified`, as the browser formats it.
|
|
373
287
|
pub modified: String,
|
|
374
288
|
/// Iframe `document.referrer`, set by the embedding page's policy or empty when absent.
|
|
@@ -381,18 +295,36 @@ pub struct Session<'session> {
|
|
|
381
295
|
pub trace: Option<Trace>,
|
|
382
296
|
/// The returned program's own trace frame.
|
|
383
297
|
pub controller: Option<&'session str>,
|
|
298
|
+
/// Definitions in the current iframe, used to translate audited globals.
|
|
299
|
+
pub globals: &'session Globals,
|
|
384
300
|
}
|
|
385
301
|
|
|
386
302
|
impl Session<'_> {
|
|
387
303
|
/// Patches a stored record while it still uses reference names: document identity, the
|
|
388
|
-
/// referrer, and the environment audit's navigation measurements.
|
|
389
|
-
fn patch(
|
|
304
|
+
/// referrer, and the environment audit's trace membership and navigation measurements.
|
|
305
|
+
fn patch(
|
|
306
|
+
&self,
|
|
307
|
+
probe: &Probe,
|
|
308
|
+
fields: &mut JsObject,
|
|
309
|
+
recorded: &BTreeMap<String, Signature>,
|
|
310
|
+
) -> Result<()> {
|
|
390
311
|
for value in fields.values_mut() {
|
|
391
312
|
refresh_transplant(value, self.iframe_url, &self.modified);
|
|
392
313
|
}
|
|
393
314
|
if let Some(referrer) = fields.get_mut(ENVIRONMENT_REFERRER) {
|
|
394
315
|
*referrer = json!(self.referrer);
|
|
395
316
|
}
|
|
317
|
+
if let Some(groups) = fields
|
|
318
|
+
.get_mut(ENVIRONMENT_AUDIT_GROUPS)
|
|
319
|
+
.and_then(JsValue::as_object_mut)
|
|
320
|
+
{
|
|
321
|
+
let trace = self
|
|
322
|
+
.trace
|
|
323
|
+
.as_ref()
|
|
324
|
+
.context("the environment audit needs the iframe's trace")?;
|
|
325
|
+
refresh_globals(groups, &self.globals.mapping(recorded), self.globals);
|
|
326
|
+
refresh_trace(trace, groups, &probe.features, self.controller)?;
|
|
327
|
+
}
|
|
396
328
|
if let Some(values) = fields
|
|
397
329
|
.get_mut(ENVIRONMENT_AUDIT_VALUES)
|
|
398
330
|
.and_then(JsValue::as_object_mut)
|
|
@@ -497,28 +429,47 @@ fn refresh_string(text: &mut String, iframe_url: &str, modified: &str) {
|
|
|
497
429
|
}
|
|
498
430
|
}
|
|
499
431
|
|
|
500
|
-
///
|
|
432
|
+
/// Substitutes this session's globals and removes unmatched names and their empty groups.
|
|
433
|
+
fn refresh_globals(groups: &mut JsObject, mapping: &BTreeMap<String, String>, globals: &Globals) {
|
|
434
|
+
for (key, members) in mem::take(groups) {
|
|
435
|
+
let JsValue::Array(members) = members else {
|
|
436
|
+
groups.insert(key, members);
|
|
437
|
+
continue;
|
|
438
|
+
};
|
|
439
|
+
if members.is_empty() {
|
|
440
|
+
group(groups, key);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for member in members {
|
|
444
|
+
let global = member
|
|
445
|
+
.as_str()
|
|
446
|
+
.and_then(|name| name.strip_prefix("o."))
|
|
447
|
+
.filter(|name| vocabulary::is_hex_name(name));
|
|
448
|
+
let Some(global) = global else {
|
|
449
|
+
group(groups, key.clone()).push(member);
|
|
450
|
+
continue;
|
|
451
|
+
};
|
|
452
|
+
let Some(current) = mapping.get(global) else {
|
|
453
|
+
continue;
|
|
454
|
+
};
|
|
455
|
+
let current_key = globals.0.get(current).and_then(Signature::audit_value);
|
|
456
|
+
let current_key = current_key.unwrap_or(&key).to_owned();
|
|
457
|
+
group(groups, current_key).push(json!(format!("o.{current}")));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// Places this session's trace global in the audit group matching its trace frames. Uses the
|
|
501
463
|
/// recovered controller frame as the second root, or the iframe callback frame if none was
|
|
502
464
|
/// recovered.
|
|
503
|
-
|
|
465
|
+
fn refresh_trace(
|
|
504
466
|
trace: &Trace,
|
|
505
467
|
groups: &mut JsObject,
|
|
506
468
|
features: &[String],
|
|
507
469
|
controller: Option<&str>,
|
|
508
470
|
) -> Result<()> {
|
|
509
471
|
let member = JsValue::String(format!("o.{}", trace.global));
|
|
510
|
-
let
|
|
511
|
-
|names: &JsValue| matches!(names, JsValue::Array(names) if names.contains(&member));
|
|
512
|
-
if !groups.values().any(traced) {
|
|
513
|
-
return Ok(());
|
|
514
|
-
}
|
|
515
|
-
let entered = features
|
|
516
|
-
.windows(2)
|
|
517
|
-
.find(|pair| pair[1] == trace.replace)
|
|
518
|
-
.map(|pair| pair[0].as_str())
|
|
519
|
-
.context("environment audit has no session trace entry")?;
|
|
520
|
-
let second = controller.unwrap_or(&trace.returned_root);
|
|
521
|
-
let key = format!("{},{},{}", trace.initial_root, second, entered);
|
|
472
|
+
let key = trace.audit_key(features, controller)?;
|
|
522
473
|
for (old_key, mut names) in mem::take(groups) {
|
|
523
474
|
let members = names
|
|
524
475
|
.as_array_mut()
|
|
@@ -532,6 +483,11 @@ pub(crate) fn refresh_trace(
|
|
|
532
483
|
group(groups, key.clone()).push(member.clone());
|
|
533
484
|
}
|
|
534
485
|
}
|
|
486
|
+
// Recorded answers name another session's trace global, which is dropped beforehand.
|
|
487
|
+
let traced = group(groups, key);
|
|
488
|
+
if !traced.contains(&member) {
|
|
489
|
+
traced.push(member);
|
|
490
|
+
}
|
|
535
491
|
Ok(())
|
|
536
492
|
}
|
|
537
493
|
|
|
@@ -578,93 +534,55 @@ fn refresh_transplant(value: &mut JsValue, iframe_url: &str, modified: &str) {
|
|
|
578
534
|
#[cfg(test)]
|
|
579
535
|
mod tests {
|
|
580
536
|
use super::*;
|
|
537
|
+
use crate::challenge::installed::Installed;
|
|
581
538
|
use crate::js;
|
|
582
539
|
|
|
583
540
|
#[test]
|
|
584
|
-
fn
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
let mut plan: ProbePlan = js::from_value(json!({
|
|
590
|
-
"record_fields":{"counter":"counter", "token":"token", "input":"input",
|
|
591
|
-
"status":"status", "message":"message", "started":"started",
|
|
592
|
-
"finished":"finished", "duration":"duration"},
|
|
593
|
-
"payload":{"template":{"fields":{}}, "published":[], "footer":{}, "pending":null},
|
|
594
|
-
"probes":probes
|
|
541
|
+
fn audit_globals_keep_their_groups_under_current_names() {
|
|
542
|
+
let mut groups: JsObject = js::from_value(json!({
|
|
543
|
+
"N": ["o._111111111111111111111111", "o.alert"],
|
|
544
|
+
"o": ["o._222222222222222222222222"],
|
|
545
|
+
"empty": []
|
|
595
546
|
}))
|
|
596
547
|
.unwrap();
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
.find(|text| text.starts_with('.'))
|
|
603
|
-
.unwrap()
|
|
604
|
-
.clone();
|
|
605
|
-
let html = format!("<style>.Abcd1{{color:red}}</style><style>{marker}{{margin:0}}</style>");
|
|
606
|
-
let snapshot = StylesheetSnapshot::capture(
|
|
607
|
-
&html,
|
|
608
|
-
vec![
|
|
609
|
-
vec![".Abcd1 { color: red; }".into()],
|
|
610
|
-
vec![format!("{marker} {{ margin: 0px; }}")],
|
|
611
|
-
],
|
|
612
|
-
)
|
|
613
|
-
.unwrap();
|
|
614
|
-
let fields = JsObject::from_iter([
|
|
615
|
-
("unchanged".into(), json!(true)),
|
|
616
|
-
(STYLESHEET_DIGEST.into(), json!("stale")),
|
|
617
|
-
(STYLESHEET_MARKER.into(), json!("stale")),
|
|
618
|
-
]);
|
|
619
|
-
plan.probes[position].writes.fields = fields.keys().cloned().collect();
|
|
620
|
-
let mut answers = Answers {
|
|
621
|
-
reference: REFERENCE.digest.clone(),
|
|
622
|
-
probes: BTreeMap::from([(
|
|
623
|
-
position + 1,
|
|
624
|
-
Stored {
|
|
625
|
-
duration: None,
|
|
626
|
-
fields,
|
|
627
|
-
late: vec![STYLESHEET_MARKER.into()],
|
|
628
|
-
},
|
|
548
|
+
refresh_globals(
|
|
549
|
+
&mut groups,
|
|
550
|
+
&BTreeMap::from([(
|
|
551
|
+
"_111111111111111111111111".into(),
|
|
552
|
+
"_aaaaaaaaaaaaaaaaaaaaaaaa".into(),
|
|
629
553
|
)]),
|
|
630
|
-
|
|
631
|
-
..Answers::default()
|
|
632
|
-
};
|
|
633
|
-
let source = Source::try_new("var root = this || self;").unwrap();
|
|
634
|
-
let installed = Installed::default();
|
|
635
|
-
let replay = Replay::new(&answers, &plan, &source, &installed).unwrap();
|
|
636
|
-
let navigation = ResponseMetrics::default();
|
|
637
|
-
let options = Options::new();
|
|
638
|
-
let mut session = Session {
|
|
639
|
-
iframe_url: CURRENT,
|
|
640
|
-
iframe_html: &html,
|
|
641
|
-
modified: "01/01/2000 00:00:00".into(),
|
|
642
|
-
referrer: "https://example.com/",
|
|
643
|
-
navigation: &navigation,
|
|
644
|
-
options: &options,
|
|
645
|
-
trace: None,
|
|
646
|
-
controller: None,
|
|
647
|
-
};
|
|
648
|
-
let result = replay.answer(&plan.probes[position], &session).unwrap();
|
|
649
|
-
assert_eq!(
|
|
650
|
-
result.early.keys().map(String::as_str).collect::<Vec<_>>(),
|
|
651
|
-
["unchanged", STYLESHEET_DIGEST]
|
|
554
|
+
&Globals::default(),
|
|
652
555
|
);
|
|
653
|
-
assert_eq!(result.late.len(), 1);
|
|
654
556
|
assert_eq!(
|
|
655
|
-
|
|
656
|
-
json!(
|
|
557
|
+
json!(groups),
|
|
558
|
+
json!({
|
|
559
|
+
"N": ["o._aaaaaaaaaaaaaaaaaaaaaaaa", "o.alert"], "empty": []
|
|
560
|
+
})
|
|
657
561
|
);
|
|
658
|
-
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[test]
|
|
565
|
+
fn installed_globals_refresh_their_values_and_split_shared_groups() {
|
|
566
|
+
let installed = |name: &str, value: &str| {
|
|
567
|
+
Globals::installed(&Installed {
|
|
568
|
+
audit: BTreeMap::from([(name.into(), value.into())]),
|
|
569
|
+
..Default::default()
|
|
570
|
+
})
|
|
571
|
+
};
|
|
572
|
+
let globals = installed("_aaaaaaaaaaaaaaaaaaaaaaaa", "_bbbbbbbbbbbbbbbbbbbbbbbb");
|
|
573
|
+
let recorded = installed("_111111111111111111111111", "_222222222222222222222222");
|
|
574
|
+
let mut groups: JsObject = js::from_value(json!({
|
|
575
|
+
"_222222222222222222222222": ["o._111111111111111111111111", "other"]
|
|
576
|
+
}))
|
|
577
|
+
.unwrap();
|
|
578
|
+
refresh_globals(&mut groups, &globals.mapping(&recorded.0), &globals);
|
|
659
579
|
assert_eq!(
|
|
660
|
-
|
|
661
|
-
json!(
|
|
580
|
+
json!(groups),
|
|
581
|
+
json!({
|
|
582
|
+
"_bbbbbbbbbbbbbbbbbbbbbbbb": ["o._aaaaaaaaaaaaaaaaaaaaaaaa"],
|
|
583
|
+
"_222222222222222222222222": ["other"]
|
|
584
|
+
})
|
|
662
585
|
);
|
|
663
|
-
session.iframe_html = "<style>changed{color:blue}</style>";
|
|
664
|
-
assert!(replay.answer(&plan.probes[position], &session).is_err());
|
|
665
|
-
drop(replay);
|
|
666
|
-
answers.reference = "wrong-reference".into();
|
|
667
|
-
assert!(Replay::new(&answers, &plan, &source, &installed).is_err());
|
|
668
586
|
}
|
|
669
587
|
|
|
670
588
|
#[test]
|
|
@@ -697,6 +615,16 @@ mod tests {
|
|
|
697
615
|
r#"{"old-root,old-response,old-probe":["other"],"fresh-root,controller-frame,fresh-probe":["o.trace"],"T":["enabled"],"empty":[]}"#
|
|
698
616
|
);
|
|
699
617
|
assert!(refreshed(&[], None).is_err());
|
|
618
|
+
let packed = [
|
|
619
|
+
"_111111111111111111111111".into(),
|
|
620
|
+
"replaceTrace".into(),
|
|
621
|
+
"fresh-probe$frame".into(),
|
|
622
|
+
];
|
|
623
|
+
assert!(
|
|
624
|
+
refreshed(&packed, None)
|
|
625
|
+
.unwrap()
|
|
626
|
+
.contains("fresh-response,fresh-probe$frame")
|
|
627
|
+
);
|
|
700
628
|
}
|
|
701
629
|
|
|
702
630
|
const CURRENT: &str = "https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/g/turnstile/f/av0/rch/y3vzw/0x4AAAAAAB8gDxRzm2UASWbL/light/fbE/new/normal?lang=auto";
|