Class: Shugoi::Core

Inherits:
Object
  • Object
show all
Defined in:
lib/shugoi/core.rb

Overview

Évaluation d'une requête : pre-flight PoW challenge, /__sg_challenge, blocage headless. Parité avec core.ts (module Node).

Defined Under Namespace

Classes: Decision

Instance Method Summary collapse

Constructor Details

#initialize(config, pow, api_client) ⇒ Core

Returns a new instance of Core.



10
11
12
13
14
15
16
17
# File 'lib/shugoi/core.rb', line 10

def initialize(config, pow, api_client)
  @config = config
  @pow = pow
  @api_client = api_client
  @validation_valid = false
  @validation_failed = false
  @validation_warned_at = 0
end

Instance Method Details

#challenge_pageObject

Page challenge (tableau en commentaire + JS PoW inline).



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/shugoi/core.rb', line 51

def challenge_page
  js = <<~JS
    (function(){
    var P=new URLSearchParams(location.search);
    var salt=P.get('salt')||'', ts=P.get('ts')||'', diff=parseInt(P.get('diff')||'10',10), path=P.get('path')||'/';
    var enc=new TextEncoder();
    function bits(d){var l=0;for(var i=0;i<d.length;i++){var b=parseInt(d[i],16);if(b===0){l+=4;continue}var s=b.toString(2),z=0;while(z<s.length&&s[z]==='0')z++;l+=z;break}return l}
    var n=0;
    function step(){
      crypto.subtle.digest('SHA-256',enc.encode(salt+':'+n.toString(16))).then(function(buf){
        var h=Array.from(new Uint8Array(buf)).map(function(v){return v.toString(16).padStart(2,'0')}).join('');
        if(bits(h)>=diff){var base=path;var q=(base.indexOf('?')>=0?'&':'?')+'sg_proof='+ts+':'+n.toString(16);location.replace(base+q)}
        else{n++;if(n<300000)step()}
      }).catch(function(){location.reload()});
    }
    step();
    })();
  JS
  html = "<!--\n#{BLOCK_PAGE}-->\n<script>#{js}</script>"
  Decision.new(status: 200, content_type: "text/html", body: html, headers: {})
end

#evaluate(ctx) ⇒ Decision?

Returns nil = laisser passer.

Parameters:

  • ctx (Hash)

    { path:, ua:, ip:, host:, accept_language:, sec_fetch_dest:, sec_fetch_mode:, sg_proof:, forwarded_prefix: }

Returns:

  • (Decision, nil)

    nil = laisser passer



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/shugoi/core.rb', line 21

def evaluate(ctx)
  path = ctx[:path].to_s
  return nil if @config.is_allowlisted?(path)

  # Route du challenge JS (le navigateur arrive ici après le 307).
  if path == "/__sg_challenge"
    return challenge_page
  end

  # Pre-flight PoW challenge (anti-curl/view-source).
  return nil if path.include?("/__shugoi/") || path.start_with?("/api/")
  ua = ctx[:ua].to_s

  if ua.match?(/Mozilla/i) && !@config.signing_secret.to_s.empty?
    proof = ctx[:sg_proof].to_s
    unless @pow.valid?(proof)
      return pow_challenge_307(ctx)
    end
  end

  # Blocage headless (UA).
  if ua.match?(/Mozilla/i) == false && @config.is_headless?(ua)
    @api_client.post_event(@config.site_key, "headless", "")
    return Decision.new(status: @config.block_status, content_type: "text/plain", body: BLOCK_PAGE, headers: {})
  end

  nil
end