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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +10 -9
  4. data/UPSTREAM.md +6 -6
  5. data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  6. data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  7. data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
  8. data/lib/cfsolve/version.rb +2 -2
  9. data/vendor/cfsolve/README.md +0 -35
  10. data/vendor/cfsolve/examples/build_profile.rs +17 -15
  11. data/vendor/cfsolve/examples/capture_session.rs +37 -0
  12. data/vendor/cfsolve/examples/check_answers.rs +66 -0
  13. data/vendor/cfsolve/examples/compare_session.rs +190 -0
  14. data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
  15. data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  16. data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  17. data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
  18. data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
  19. data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
  20. data/vendor/cfsolve/src/capture/profile.rs +7 -18
  21. data/vendor/cfsolve/src/capture/recorded.rs +42 -7
  22. data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
  23. data/vendor/cfsolve/src/challenge/installed.rs +122 -271
  24. data/vendor/cfsolve/src/challenge/payload.rs +107 -5
  25. data/vendor/cfsolve/src/challenge/state.rs +98 -16
  26. data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
  27. data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
  28. data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
  29. data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
  30. data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
  31. data/vendor/cfsolve/src/http/client.rs +11 -0
  32. data/vendor/cfsolve/src/js/ast.rs +12 -3
  33. data/vendor/cfsolve/src/js/objects.rs +9 -2
  34. data/vendor/cfsolve/src/listing/fold.rs +0 -83
  35. data/vendor/cfsolve/src/listing/query.rs +25 -244
  36. data/vendor/cfsolve/src/listing.rs +0 -1
  37. data/vendor/cfsolve/src/session/initial.rs +14 -5
  38. data/vendor/cfsolve/src/session/probes.rs +160 -125
  39. data/vendor/cfsolve/src/session/timing.rs +58 -53
  40. data/vendor/cfsolve/src/session/widget.rs +5 -6
  41. data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
  42. data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
  43. data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
  44. data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
  45. data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
  46. data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
  47. data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
  48. data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
  49. data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
  50. data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
  51. data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
  52. data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
  53. data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
  54. data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
  55. data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
  56. data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
  57. data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
  58. data/vendor/cfsolve/src/stage2/probes.rs +0 -1
  59. data/vendor/cfsolve/src/stage2/records.rs +4 -13
  60. data/vendor/cfsolve/src/stage2/reference.json +1 -1
  61. data/vendor/cfsolve/src/stage2/reference.rs +98 -100
  62. data/vendor/cfsolve/src/stage2/registry.rs +41 -34
  63. data/vendor/cfsolve/src/stage2/replay.rs +182 -254
  64. data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
  65. data/vendor/cfsolve/src/stage2/trace.rs +18 -0
  66. data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
  67. data/vendor/cfsolve/src/stage2.rs +2 -1
  68. data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
  69. data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
  70. data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
  71. data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
  72. data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
  73. data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
  74. data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
  75. data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
  76. data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
  77. data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
  78. data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
  79. data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
  80. data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
  81. data/vendor/cfsolve/src/vm/interpret.rs +7 -2
  82. data/vendor/cfsolve/src/vm/machine.rs +6 -2
  83. data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
  84. data/vendor/cfsolve/src/vm/session.rs +36 -14
  85. data/vendor/cfsolve/src/vm/value.rs +13 -0
  86. data/vendor/cfsolve/src/vm.rs +1 -0
  87. data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
  88. metadata +13 -6
  89. data/vendor/cfsolve/src/listing/normalize.rs +0 -264
  90. data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
  91. data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
  92. data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
  93. 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, IfStmt, ObjectLit,
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, mapped to the option each returns.
40
- pub getters: BTreeMap<String, 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<Getter>,
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
- struct Getter {
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
- self.getters.push(Getter {
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
- /// Rejects a getter name that returns different options.
593
- fn distinct_getters(getters: Vec<Getter>) -> Result<BTreeMap<String, String>> {
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 Getter { function, option } in getters {
596
- let previous = distinct.entry(function.clone()).or_insert(option.clone());
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 == option,
599
- "getter {function} reads both {previous} and {option}"
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
- let encoded = super::STANDARD.encode([255 ^ 0x05, 0x00 ^ 0xe7, 0xd8 ^ 0x05]);
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(literal) if !matches!(literal, Lit::Regex(_)) => {
78
- self.literals.insert(name, value.clone());
79
- }
80
- Expr::Tpl(template) if template.exprs.is_empty() => {
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
- Expr::Unary(unary) if unary.op != UnaryOp::Delete => eager_operands(&unary.arg),
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
- Box::new(Expr::Assign(AssignExpr {
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) = &self.challenge_page {
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, SyntaxContext,
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, Serialize)]
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 {