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
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
//! tally ([`super::tally`]), the sender's additions and POST log, and the resource timing
|
|
3
3
|
//! format ([`super::reduced_timing`]).
|
|
4
4
|
|
|
5
|
-
use std::collections::BTreeMap;
|
|
5
|
+
use std::collections::{BTreeMap, HashMap};
|
|
6
6
|
|
|
7
7
|
use anyhow::{Context, Result, bail, ensure};
|
|
8
8
|
use indexmap::IndexMap;
|
|
9
9
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
10
10
|
use swc_core::ecma::ast::{
|
|
11
|
-
ArrowExpr, AssignExpr, AssignTarget, BinaryOp, CallExpr, Expr, Function,
|
|
12
|
-
SimpleAssignTarget, Stmt, VarDeclarator,
|
|
11
|
+
ArrowExpr, AssignExpr, AssignTarget, BinaryOp, CallExpr, Expr, FnDecl, Function, Id, IfStmt,
|
|
12
|
+
MemberExpr, ObjectLit, SimpleAssignTarget, Stmt, VarDeclarator,
|
|
13
13
|
};
|
|
14
14
|
use swc_core::ecma::visit::{Visit, VisitWith};
|
|
15
15
|
|
|
@@ -36,11 +36,21 @@ pub struct ScriptState {
|
|
|
36
36
|
/// Options copied into the payload under their original names at send time.
|
|
37
37
|
pub forwarded: Vec<String>,
|
|
38
38
|
pub timing: Option<ResourceTiming>,
|
|
39
|
-
/// Functions on the script's namespace,
|
|
40
|
-
pub getters: BTreeMap<String,
|
|
39
|
+
/// Functions on the script's namespace, by name.
|
|
40
|
+
pub getters: BTreeMap<String, Getter>,
|
|
41
41
|
pub post_log: Option<PostLog>,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/// A function on the script's namespace that returns an option.
|
|
45
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
46
|
+
pub struct Getter {
|
|
47
|
+
pub option: String,
|
|
48
|
+
/// The option holding the page origin, when the getter passes each render sample's stack
|
|
49
|
+
/// through a helper that replaces URLs on that origin with indices (see
|
|
50
|
+
/// [`crate::challenge::payload::split_page_urls`]).
|
|
51
|
+
pub page_origin: Option<String>,
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/// Log of challenge POSTs. Each payload includes the log so far. The sender then adds an entry for
|
|
45
55
|
/// the current POST and records its response time when the response arrives.
|
|
46
56
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -208,7 +218,7 @@ impl ScriptState {
|
|
|
208
218
|
elapsed: elapsed.map(|(_, elapsed)| elapsed),
|
|
209
219
|
forwarded: forwarded.into_iter().map(|(_, key)| key).collect(),
|
|
210
220
|
timing,
|
|
211
|
-
getters: distinct_getters(sender.getters)?,
|
|
221
|
+
getters: distinct_getters(sender.getters, &sender.helpers)?,
|
|
212
222
|
post_log,
|
|
213
223
|
})
|
|
214
224
|
}
|
|
@@ -400,16 +410,56 @@ struct Sender {
|
|
|
400
410
|
scopes: Vec<usize>,
|
|
401
411
|
elapsed: Vec<(usize, Elapsed)>,
|
|
402
412
|
copies: Vec<(usize, String)>,
|
|
403
|
-
getters: Vec<
|
|
413
|
+
getters: Vec<FoundGetter>,
|
|
414
|
+
/// The first option each declared function reads, by binding.
|
|
415
|
+
helpers: HashMap<Id, String>,
|
|
404
416
|
pushes: Vec<Push>,
|
|
405
417
|
entries: Vec<LogEntry>,
|
|
406
418
|
stamps: Vec<Stamp>,
|
|
407
419
|
}
|
|
408
420
|
|
|
409
|
-
/// A namespace getter named `function` that returns the option `option
|
|
410
|
-
|
|
421
|
+
/// A namespace getter named `function` that returns the option `option`, rewriting each
|
|
422
|
+
/// sample's stack with the declared function `rewrite`, if any.
|
|
423
|
+
struct FoundGetter {
|
|
411
424
|
function: String,
|
|
412
425
|
option: String,
|
|
426
|
+
rewrite: Option<Id>,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Finds `<sample>.s = <helper>(<sample>.s)`.
|
|
430
|
+
#[derive(Default)]
|
|
431
|
+
struct StackRewrite(Option<Id>);
|
|
432
|
+
|
|
433
|
+
impl Visit for StackRewrite {
|
|
434
|
+
fn visit_assign_expr(&mut self, assignment: &AssignExpr) {
|
|
435
|
+
if let AssignTarget::Simple(SimpleAssignTarget::Member(target)) = &assignment.left
|
|
436
|
+
&& ast::ident_property(target) == Some("s")
|
|
437
|
+
&& let Expr::Call(call) = &*assignment.right
|
|
438
|
+
&& let Some(helper) = call.callee.as_expr().and_then(|callee| callee.as_ident())
|
|
439
|
+
&& let [argument] = &call.args[..]
|
|
440
|
+
&& let Some(read) = argument.expr.as_member()
|
|
441
|
+
&& ast::ident_property(read) == Some("s")
|
|
442
|
+
{
|
|
443
|
+
self.0 = Some(helper.to_id());
|
|
444
|
+
}
|
|
445
|
+
assignment.visit_children_with(self);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// The first option a function reads.
|
|
450
|
+
#[derive(Default)]
|
|
451
|
+
struct OptionRead(Option<String>);
|
|
452
|
+
|
|
453
|
+
impl Visit for OptionRead {
|
|
454
|
+
fn visit_member_expr(&mut self, member: &MemberExpr) {
|
|
455
|
+
if self.0.is_none()
|
|
456
|
+
&& expression::is_options(&member.obj)
|
|
457
|
+
&& let Some(option) = ast::member_str(&member.prop)
|
|
458
|
+
{
|
|
459
|
+
self.0 = Some(option.to_owned());
|
|
460
|
+
}
|
|
461
|
+
member.visit_children_with(self);
|
|
462
|
+
}
|
|
413
463
|
}
|
|
414
464
|
|
|
415
465
|
/// Matches `_cf_chl_opt.<log>.push(<local>)`.
|
|
@@ -503,6 +553,15 @@ impl Visit for Sender {
|
|
|
503
553
|
self.within(function);
|
|
504
554
|
}
|
|
505
555
|
|
|
556
|
+
fn visit_fn_decl(&mut self, declaration: &FnDecl) {
|
|
557
|
+
let mut read = OptionRead::default();
|
|
558
|
+
declaration.function.visit_with(&mut read);
|
|
559
|
+
if let Some(option) = read.0 {
|
|
560
|
+
self.helpers.insert(declaration.ident.to_id(), option);
|
|
561
|
+
}
|
|
562
|
+
declaration.visit_children_with(self);
|
|
563
|
+
}
|
|
564
|
+
|
|
506
565
|
fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) {
|
|
507
566
|
self.within(arrow);
|
|
508
567
|
}
|
|
@@ -563,9 +622,12 @@ impl Visit for Sender {
|
|
|
563
622
|
&& let Expr::Assign(first) = &*first.expr
|
|
564
623
|
&& let Some(option) = expression::option_key(&first.right) =>
|
|
565
624
|
{
|
|
566
|
-
|
|
625
|
+
let mut rewrite = StackRewrite::default();
|
|
626
|
+
getter.function.visit_with(&mut rewrite);
|
|
627
|
+
self.getters.push(FoundGetter {
|
|
567
628
|
function: key.to_owned(),
|
|
568
629
|
option: option.to_owned(),
|
|
630
|
+
rewrite: rewrite.0,
|
|
569
631
|
});
|
|
570
632
|
}
|
|
571
633
|
// `payload.<key> = _cf_chl_opt.<key>`
|
|
@@ -589,14 +651,34 @@ impl Visit for Sender {
|
|
|
589
651
|
}
|
|
590
652
|
}
|
|
591
653
|
|
|
592
|
-
///
|
|
593
|
-
|
|
654
|
+
/// Resolves each getter's stack rewrite to the option its helper reads. Rejects a getter name
|
|
655
|
+
/// that returns different options.
|
|
656
|
+
fn distinct_getters(
|
|
657
|
+
getters: Vec<FoundGetter>,
|
|
658
|
+
helpers: &HashMap<Id, String>,
|
|
659
|
+
) -> Result<BTreeMap<String, Getter>> {
|
|
594
660
|
let mut distinct = BTreeMap::new();
|
|
595
|
-
for
|
|
596
|
-
let
|
|
661
|
+
for found in getters {
|
|
662
|
+
let function = found.function;
|
|
663
|
+
let page_origin = found
|
|
664
|
+
.rewrite
|
|
665
|
+
.map(|helper| {
|
|
666
|
+
helpers.get(&helper).cloned().with_context(|| {
|
|
667
|
+
format!(
|
|
668
|
+
"getter {function} rewrites stacks with unknown helper {}",
|
|
669
|
+
helper.0
|
|
670
|
+
)
|
|
671
|
+
})
|
|
672
|
+
})
|
|
673
|
+
.transpose()?;
|
|
674
|
+
let getter = Getter {
|
|
675
|
+
option: found.option,
|
|
676
|
+
page_origin,
|
|
677
|
+
};
|
|
678
|
+
let previous = distinct.entry(function.clone()).or_insert(getter.clone());
|
|
597
679
|
ensure!(
|
|
598
|
-
*previous ==
|
|
599
|
-
"getter {function} reads both {previous} and {
|
|
680
|
+
*previous == getter,
|
|
681
|
+
"getter {function} reads both {previous:?} and {getter:?}"
|
|
600
682
|
);
|
|
601
683
|
}
|
|
602
684
|
Ok(distinct)
|
|
@@ -348,8 +348,9 @@ mod tests {
|
|
|
348
348
|
fn decodes_escaped_code_units() {
|
|
349
349
|
use base64::Engine;
|
|
350
350
|
|
|
351
|
-
// 255 escapes the code unit U+D800, a lone surrogate.
|
|
352
|
-
|
|
351
|
+
// 255 escapes the code unit U+D800, a lone surrogate. The bytes 255, 0x00, 0xd8 are
|
|
352
|
+
// XORed with the key 59141 (0xe705), low byte first.
|
|
353
|
+
let encoded = super::STANDARD.encode([255 ^ 0x05, 0xe7, 0xd8 ^ 0x05]);
|
|
353
354
|
let decoded = super::decode(&encoded, 59141).unwrap();
|
|
354
355
|
assert_eq!(
|
|
355
356
|
decoded.as_wtf8().to_ill_formed_utf16().collect::<Vec<_>>(),
|
|
@@ -74,10 +74,10 @@ impl ObjectProperties {
|
|
|
74
74
|
}
|
|
75
75
|
// A regular expression carries state and identity, so it cannot
|
|
76
76
|
// stand in for every read.
|
|
77
|
-
Expr::Lit(
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
Expr::
|
|
77
|
+
Expr::Lit(Lit::Regex(_)) => {}
|
|
78
|
+
// Decoded strings may print as templates without substitutions.
|
|
79
|
+
Expr::Tpl(template) if !template.exprs.is_empty() => {}
|
|
80
|
+
Expr::Lit(_) | Expr::Tpl(_) => {
|
|
81
81
|
self.literals.insert(name, value.clone());
|
|
82
82
|
}
|
|
83
83
|
_ => {}
|
|
@@ -489,7 +489,10 @@ fn commutes(first: &Expr, second: &Expr, globals: Globals) -> bool {
|
|
|
489
489
|
fn eager_operands(result: &Expr) -> Option<Vec<&Expr>> {
|
|
490
490
|
match result {
|
|
491
491
|
Expr::Paren(paren) => eager_operands(&paren.expr),
|
|
492
|
-
|
|
492
|
+
// A unary operator applies to its evaluated operand, as the call would.
|
|
493
|
+
Expr::Unary(unary) if unary.op != UnaryOp::Delete => {
|
|
494
|
+
eager_operands(&unary.arg).or_else(|| Some(vec![&unary.arg]))
|
|
495
|
+
}
|
|
493
496
|
Expr::Bin(binary) if !binary.op.may_short_circuit() => {
|
|
494
497
|
Some(vec![&binary.left, &binary.right])
|
|
495
498
|
}
|
|
@@ -776,33 +779,6 @@ mod tests {
|
|
|
776
779
|
assert!(!output.contains("(0, plain)"), "{output}");
|
|
777
780
|
}
|
|
778
781
|
|
|
779
|
-
#[test]
|
|
780
|
-
fn inlines_static_template_properties_without_evaluating_interpolation() {
|
|
781
|
-
let output = transform(
|
|
782
|
-
r"function f() {
|
|
783
|
-
var helper;
|
|
784
|
-
helper = { first: `fixed` };
|
|
785
|
-
helper.second = `line\n\u{1f600}\ud800`;
|
|
786
|
-
helper.dynamic = `value:${read()}`;
|
|
787
|
-
return function() { return [helper.first, helper.second, helper.dynamic]; };
|
|
788
|
-
}",
|
|
789
|
-
);
|
|
790
|
-
testing::assert_contains(
|
|
791
|
-
&output,
|
|
792
|
-
&[],
|
|
793
|
-
&[r"return [`fixed`, `line\n\u{1f600}\ud800`, helper.dynamic]"],
|
|
794
|
-
);
|
|
795
|
-
let output = transform(
|
|
796
|
-
r"function f() {
|
|
797
|
-
var helper;
|
|
798
|
-
helper = { name: `before` };
|
|
799
|
-
helper.name = `after`;
|
|
800
|
-
return helper.name;
|
|
801
|
-
}",
|
|
802
|
-
);
|
|
803
|
-
assert!(output.contains("return helper.name;"), "{output}");
|
|
804
|
-
}
|
|
805
|
-
|
|
806
782
|
#[test]
|
|
807
783
|
fn inlines_member_assigned_properties_and_helper_functions() {
|
|
808
784
|
let output = transform(
|
|
@@ -811,6 +787,15 @@ mod tests {
|
|
|
811
787
|
);
|
|
812
788
|
assert!(output.contains("(1) + (2)"), "{output}");
|
|
813
789
|
testing::assert_lacks(&output, &["p.name", "h.name]"]);
|
|
790
|
+
// Static templates inline into later callbacks, as in a payload built after a delay.
|
|
791
|
+
let output = transform(
|
|
792
|
+
"function f(h, g) { h = { key: `_field` }; g(function () { return w[h.key]; }); }",
|
|
793
|
+
);
|
|
794
|
+
assert!(output.contains("w[`_field`]"), "{output}");
|
|
795
|
+
// A unary helper evaluates its argument first either way, even a member read.
|
|
796
|
+
let output =
|
|
797
|
+
transform("function f(n) { n = function (a) { return !a; }; if (n(w.x)) g(); }");
|
|
798
|
+
assert!(output.contains("!(w.x)"), "{output}");
|
|
814
799
|
// Helper functions inline when they name only what callers see.
|
|
815
800
|
let output = transform(
|
|
816
801
|
"function f(add, call, read, g) { add = function(a, b) { return a + b; }; \
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
//! expression so its side effects still run.
|
|
15
15
|
//!
|
|
16
16
|
//! Undefined slots need no assignment if the literal runs at most once per call, in the
|
|
17
|
-
//! declaring function and outside loops. The replacement locals already start undefined.
|
|
17
|
+
//! declaring function and outside loops. The replacement locals already start undefined. A
|
|
18
|
+
//! `void` element there keeps only its operand, so a constant hidden as `void (k ^ x ^ (k ^ x))`
|
|
19
|
+
//! leaves the slot's later write as its local's only one, which copy propagation can inline.
|
|
18
20
|
|
|
19
21
|
use swc_core::common::util::take::Take;
|
|
20
22
|
use swc_core::common::{DUMMY_SP, Mark, SyntaxContext};
|
|
@@ -394,13 +396,19 @@ impl Split<'_> {
|
|
|
394
396
|
let mut assignments: Vec<Box<Expr>> = self
|
|
395
397
|
.initial(array, literal)
|
|
396
398
|
.into_iter()
|
|
397
|
-
.map(|(local, value)| {
|
|
398
|
-
|
|
399
|
+
.map(|(local, value)| match *value {
|
|
400
|
+
// The local already starts undefined, so only the operand runs.
|
|
401
|
+
Expr::Unary(UnaryExpr {
|
|
402
|
+
op: UnaryOp::Void,
|
|
403
|
+
arg,
|
|
404
|
+
..
|
|
405
|
+
}) if !array.assign_undefined => arg,
|
|
406
|
+
value => Box::new(Expr::Assign(AssignExpr {
|
|
399
407
|
span: DUMMY_SP,
|
|
400
408
|
op: AssignOp::Assign,
|
|
401
409
|
left: AssignTarget::Simple(SimpleAssignTarget::Ident(local.into())),
|
|
402
|
-
right: value,
|
|
403
|
-
}))
|
|
410
|
+
right: Box::new(value),
|
|
411
|
+
})),
|
|
404
412
|
})
|
|
405
413
|
.collect();
|
|
406
414
|
Some(match assignments.len() {
|
|
@@ -495,6 +503,11 @@ mod tests {
|
|
|
495
503
|
);
|
|
496
504
|
testing::assert_contains(&output, &[], &["h(r_0);", "r_0 = i;"]);
|
|
497
505
|
testing::assert_lacks(&output, &["r_0 = void 0", "["]);
|
|
506
|
+
// A `void` element runs its operand without writing the local.
|
|
507
|
+
let output =
|
|
508
|
+
transform("function f() { var r; r = [void (7 ^ k ^ (7 ^ k))]; r[0] = 1; g(r[0]); }");
|
|
509
|
+
testing::assert_contains(&output, &[], &["7 ^ k ^ (7 ^ k);", "r_0 = 1;", "g(r_0);"]);
|
|
510
|
+
testing::assert_lacks(&output, &["r_0 = void"]);
|
|
498
511
|
// A creation in a loop resets the slots on every iteration.
|
|
499
512
|
let output =
|
|
500
513
|
transform("function f() { var r; while (g()) { r = [void 0]; h(r[0]); r[0] = 1; } }");
|
|
@@ -434,20 +434,6 @@ pub(crate) fn widget_id() -> String {
|
|
|
434
434
|
#[cfg(test)]
|
|
435
435
|
mod tests {
|
|
436
436
|
use super::*;
|
|
437
|
-
use crate::challenge::reduced_timing::Measure;
|
|
438
|
-
|
|
439
|
-
fn page() -> RenderedPage {
|
|
440
|
-
RenderedPage::render(
|
|
441
|
-
r#"<html><head><title>t</title>
|
|
442
|
-
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script></head>
|
|
443
|
-
<body><div class="cf-turnstile" data-sitekey="0x4AAA" data-action="login"
|
|
444
|
-
data-theme="dark" data-size="compact" data-language="de"></div></body></html>"#,
|
|
445
|
-
Url::parse("https://example.com/login").unwrap(),
|
|
446
|
-
"j5gbk",
|
|
447
|
-
None,
|
|
448
|
-
)
|
|
449
|
-
.unwrap()
|
|
450
|
-
}
|
|
451
437
|
|
|
452
438
|
#[test]
|
|
453
439
|
fn finds_build_hashes_and_routes_only_where_the_loader_keeps_them() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! Reconstructs the page after `api.js` renders a widget. Builds the `wPr` page summary sent in
|
|
2
2
|
//! the `requestExtraParams` reply.
|
|
3
3
|
|
|
4
|
-
use std::collections::HashMap;
|
|
4
|
+
use std::collections::{BTreeMap, HashMap};
|
|
5
5
|
use std::iter;
|
|
6
6
|
|
|
7
7
|
use anyhow::{Context, Result};
|
|
@@ -40,6 +40,9 @@ pub(crate) struct Layout {
|
|
|
40
40
|
/// Widget position set by a challenge page's stylesheet for this viewport. Overrides the
|
|
41
41
|
/// other position settings.
|
|
42
42
|
pub challenge_page: Option<Placement>,
|
|
43
|
+
/// Widget positions set by embedding pages' stylesheets for this viewport, measured in the
|
|
44
|
+
/// browser, by page URL. Each overrides the other position settings on its page.
|
|
45
|
+
pub embedding_pages: BTreeMap<String, Placement>,
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
impl Default for Layout {
|
|
@@ -53,6 +56,7 @@ impl Default for Layout {
|
|
|
53
56
|
widget_right: None,
|
|
54
57
|
widget_left: 8.0,
|
|
55
58
|
challenge_page: None,
|
|
59
|
+
embedding_pages: BTreeMap::new(),
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -69,8 +73,14 @@ impl Layout {
|
|
|
69
73
|
/// Layout settings for a widget positioned by a challenge page's stylesheet.
|
|
70
74
|
#[must_use]
|
|
71
75
|
pub(crate) fn on_challenge_page(&self) -> Self {
|
|
76
|
+
self.placed(self.challenge_page.as_ref())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Layout settings for a widget at `placement`, if any.
|
|
80
|
+
#[must_use]
|
|
81
|
+
pub(crate) fn placed(&self, placement: Option<&Placement>) -> Self {
|
|
72
82
|
let mut layout = self.clone();
|
|
73
|
-
if let Some(placement) =
|
|
83
|
+
if let Some(placement) = placement {
|
|
74
84
|
layout.widget_top = placement.top;
|
|
75
85
|
layout.widget_left = placement.left;
|
|
76
86
|
layout.widget_right = Some(placement.right);
|
|
@@ -950,6 +960,22 @@ mod tests {
|
|
|
950
960
|
json!(page.signals(&Layout::default(), None))
|
|
951
961
|
}
|
|
952
962
|
|
|
963
|
+
#[test]
|
|
964
|
+
fn placements_override_the_widget_position() {
|
|
965
|
+
let placement = Placement {
|
|
966
|
+
top: 166.875,
|
|
967
|
+
left: 363.0,
|
|
968
|
+
right: 1003.0,
|
|
969
|
+
};
|
|
970
|
+
let placed = Layout::default().placed(Some(&placement));
|
|
971
|
+
assert_eq!(
|
|
972
|
+
(placed.widget_top, placed.widget_left, placed.widget_right),
|
|
973
|
+
(166.875, 363.0, Some(1003.0))
|
|
974
|
+
);
|
|
975
|
+
let unplaced = Layout::default().placed(None);
|
|
976
|
+
assert_eq!((unplaced.widget_top, unplaced.widget_right), (8.0, None));
|
|
977
|
+
}
|
|
978
|
+
|
|
953
979
|
#[test]
|
|
954
980
|
fn reproduces_a_captured_sessions_page_signals() {
|
|
955
981
|
let layout = Layout {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
use std::collections::BTreeMap;
|
|
4
4
|
use std::sync::Arc;
|
|
5
5
|
use std::time::Duration;
|
|
6
|
+
use std::{env, fs};
|
|
6
7
|
|
|
7
8
|
use anyhow::{Context, Result, ensure};
|
|
8
9
|
#[cfg(feature = "internals")]
|
|
@@ -13,6 +14,7 @@ use wreq::cookie::{Cookie, Jar};
|
|
|
13
14
|
use wreq::header::HeaderMap;
|
|
14
15
|
use wreq::http2::{StreamDependency, StreamId};
|
|
15
16
|
use wreq::redirect::Policy;
|
|
17
|
+
use wreq::tls::trust::CertStore;
|
|
16
18
|
use wreq::ws::{WebSocket, WebSocketRequestBuilder};
|
|
17
19
|
use wreq::{Client, Proxy, RequestBuilder, Response};
|
|
18
20
|
#[cfg(feature = "internals")]
|
|
@@ -24,6 +26,10 @@ use super::decode;
|
|
|
24
26
|
use super::header_order::{HeaderOrder, RequestKind, Slot};
|
|
25
27
|
use crate::engine::Engine;
|
|
26
28
|
|
|
29
|
+
/// Environment variable naming a PEM certificate to trust instead of the built-in roots, such as
|
|
30
|
+
/// `capture-proxy`'s CA, so a solve can be recorded in the same format as a browser session.
|
|
31
|
+
const PROXY_CA: &str = "CFSOLVE_PROXY_CA";
|
|
32
|
+
|
|
27
33
|
/// HTTP client configured from the browser fingerprint. Shared by all session requests to
|
|
28
34
|
/// preserve cookies and transport settings.
|
|
29
35
|
pub struct BrowserClient {
|
|
@@ -113,6 +119,11 @@ impl BrowserClient {
|
|
|
113
119
|
if let Some(proxy) = proxy {
|
|
114
120
|
builder = builder.proxy(Proxy::all(proxy)?);
|
|
115
121
|
}
|
|
122
|
+
if let Some(path) = env::var_os(PROXY_CA) {
|
|
123
|
+
let pem = fs::read(&path)
|
|
124
|
+
.with_context(|| format!("reading {PROXY_CA} {}", path.to_string_lossy()))?;
|
|
125
|
+
builder = builder.tls_cert_store(CertStore::from_pem_certs([&pem[..]])?);
|
|
126
|
+
}
|
|
116
127
|
Ok(Self {
|
|
117
128
|
http: builder.build()?,
|
|
118
129
|
jar,
|
|
@@ -9,7 +9,8 @@ use anyhow::{Result, ensure};
|
|
|
9
9
|
use swc_core::atoms::Wtf8Atom;
|
|
10
10
|
use swc_core::common::sync::Lrc;
|
|
11
11
|
use swc_core::common::{
|
|
12
|
-
BytePos, DUMMY_SP, FileName, SourceFile, SourceMap, Spanned,
|
|
12
|
+
BytePos, DUMMY_SP, FileName, GLOBALS, Globals, Mark, SourceFile, SourceMap, Spanned,
|
|
13
|
+
SyntaxContext,
|
|
13
14
|
};
|
|
14
15
|
use swc_core::ecma::ast::{
|
|
15
16
|
AssignExpr, AssignOp, BinaryOp, BlockStmt, Expr, ExprOrSpread, ExprStmt, FnDecl, Function, Id,
|
|
@@ -17,7 +18,8 @@ use swc_core::ecma::ast::{
|
|
|
17
18
|
PropOrSpread, Script, Stmt, UnaryOp, VarDeclarator,
|
|
18
19
|
};
|
|
19
20
|
use swc_core::ecma::parser::{Parser, StringInput, Syntax};
|
|
20
|
-
use swc_core::ecma::visit::{Visit, VisitWith};
|
|
21
|
+
use swc_core::ecma::visit::{Visit, VisitMutWith, VisitWith};
|
|
22
|
+
use swc_ecma_transforms::resolver;
|
|
21
23
|
|
|
22
24
|
use crate::engine::{self, Lines};
|
|
23
25
|
|
|
@@ -53,8 +55,15 @@ impl ParsedScript {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/// Parses the deobfuscated iframe script and resolves its scopes, so a binding's [`Id`]
|
|
59
|
+
/// identifies it even where other scopes reuse its name.
|
|
56
60
|
pub(crate) fn parse_iframe_javascript(source: &str) -> Result<ParsedScript> {
|
|
57
|
-
parse_javascript("deobfuscated iframe", source)
|
|
61
|
+
let mut parsed = parse_javascript("deobfuscated iframe", source)?;
|
|
62
|
+
GLOBALS.set(&Globals::new(), || {
|
|
63
|
+
let mut resolve = resolver(Mark::new(), Mark::new(), false);
|
|
64
|
+
parsed.script.visit_mut_with(&mut resolve);
|
|
65
|
+
});
|
|
66
|
+
Ok(parsed)
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/// Parses `source`, using `what` to identify it in errors.
|
|
@@ -3,17 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
use std::ops::{Deref, DerefMut};
|
|
5
5
|
|
|
6
|
-
use serde::{Deserialize, Serialize};
|
|
6
|
+
use serde::{Deserialize, Serialize, Serializer};
|
|
7
7
|
|
|
8
8
|
use crate::js::{JsObject, JsValue};
|
|
9
9
|
|
|
10
10
|
macro_rules! object {
|
|
11
11
|
($(#[$doc:meta])* $name:ident) => {
|
|
12
12
|
$(#[$doc])*
|
|
13
|
-
#[derive(Clone, Debug, Default, PartialEq, Deserialize
|
|
13
|
+
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
|
|
14
14
|
#[serde(transparent)]
|
|
15
15
|
pub struct $name(JsObject);
|
|
16
16
|
|
|
17
|
+
/// Serializes as `JSON.stringify` does, leaving out `undefined` properties.
|
|
18
|
+
impl Serialize for $name {
|
|
19
|
+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
20
|
+
serializer.collect_map(self.0.iter().filter(|(_, value)| !value.is_undefined()))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
impl $name {
|
|
18
25
|
#[must_use]
|
|
19
26
|
pub fn new() -> Self {
|
|
@@ -163,89 +163,6 @@ impl<X: Clone> Frame<X> {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/// Newer initial programs inline their string decoder: charCodeAt builds a
|
|
167
|
-
/// length and slice removes the descriptor header and padding. Fold only known
|
|
168
|
-
/// values, without executing the script or guessing randomized global names.
|
|
169
|
-
pub(crate) fn constant<X: Clone>(frame: &Frame<X>, value: &Term) -> Option<JsValue> {
|
|
170
|
-
if let Some(Known::Literal(value) | Known::Decoded(value)) = frame.resolve(value) {
|
|
171
|
-
return Some(value);
|
|
172
|
-
}
|
|
173
|
-
match value {
|
|
174
|
-
Term::Binary(operator, left, right) => {
|
|
175
|
-
let left = constant(frame, left)?;
|
|
176
|
-
let right = constant(frame, right)?;
|
|
177
|
-
match (operator, left, right) {
|
|
178
|
-
(BinaryOp::Add, JsValue::String(left), JsValue::String(right)) => {
|
|
179
|
-
Some(JsValue::String(left + right.as_str()))
|
|
180
|
-
}
|
|
181
|
-
(BinaryOp::Add, JsValue::Number(left), JsValue::Number(right)) => {
|
|
182
|
-
Some(JsValue::Number(left + right))
|
|
183
|
-
}
|
|
184
|
-
(BinaryOp::Mul, JsValue::Number(left), JsValue::Number(right)) => {
|
|
185
|
-
Some(JsValue::Number(left * right))
|
|
186
|
-
}
|
|
187
|
-
_ => None,
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
Term::Call {
|
|
191
|
-
callee,
|
|
192
|
-
this,
|
|
193
|
-
arguments,
|
|
194
|
-
} => {
|
|
195
|
-
let Term::Get(object, method) = &**callee else {
|
|
196
|
-
return None;
|
|
197
|
-
};
|
|
198
|
-
let object = constant(frame, object)?;
|
|
199
|
-
// The method must belong to a known string, but an explicit receiver can differ.
|
|
200
|
-
object.as_str()?;
|
|
201
|
-
let receiver = match this {
|
|
202
|
-
Some(this) => constant(frame, this)?,
|
|
203
|
-
None => object,
|
|
204
|
-
};
|
|
205
|
-
let units: Vec<u16> = receiver.as_str()?.encode_utf16().collect();
|
|
206
|
-
let method = constant(frame, method)?;
|
|
207
|
-
let arguments: Option<Vec<JsValue>> = arguments
|
|
208
|
-
.iter()
|
|
209
|
-
.map(|argument| constant(frame, argument))
|
|
210
|
-
.collect();
|
|
211
|
-
let arguments = arguments?;
|
|
212
|
-
match (method.as_str()?, arguments.as_slice()) {
|
|
213
|
-
("charCodeAt", [JsValue::Number(position)]) => {
|
|
214
|
-
let position = if position.is_nan() {
|
|
215
|
-
0.0
|
|
216
|
-
} else {
|
|
217
|
-
position.trunc()
|
|
218
|
-
};
|
|
219
|
-
let code = if position >= 0.0 && position < units.len() as f64 {
|
|
220
|
-
f64::from(units[position as usize])
|
|
221
|
-
} else {
|
|
222
|
-
f64::NAN
|
|
223
|
-
};
|
|
224
|
-
Some(JsValue::Number(code))
|
|
225
|
-
}
|
|
226
|
-
("slice", [JsValue::Number(start), JsValue::Number(end)]) => {
|
|
227
|
-
let index = |value: f64| {
|
|
228
|
-
let value = if value.is_nan() { 0.0 } else { value.trunc() };
|
|
229
|
-
let value = if value < 0.0 {
|
|
230
|
-
units.len() as f64 + value
|
|
231
|
-
} else {
|
|
232
|
-
value
|
|
233
|
-
};
|
|
234
|
-
value.clamp(0.0, units.len() as f64) as usize
|
|
235
|
-
};
|
|
236
|
-
let start = index(*start);
|
|
237
|
-
let end = index(*end).max(start);
|
|
238
|
-
String::from_utf16(&units[start..end])
|
|
239
|
-
.ok()
|
|
240
|
-
.map(JsValue::String)
|
|
241
|
-
}
|
|
242
|
-
_ => None,
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
_ => None,
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
166
|
/// Intersects register facts from incoming paths. Temporaries do not cross instruction boundaries.
|
|
250
167
|
impl<X: Clone + PartialEq> Facts for Frame<X> {
|
|
251
168
|
fn meet(&mut self, other: &Self) -> bool {
|