shugoi 0.4.5 → 0.4.7
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 +15 -0
- data/README.md +3 -2
- data/lib/shugoi/api_client.rb +11 -22
- data/lib/shugoi/bot_policy.rb +15 -0
- data/lib/shugoi/bot_verifier.rb +3 -12
- data/lib/shugoi/challenge_limiter.rb +38 -0
- data/lib/shugoi/challenge_path.rb +12 -0
- data/lib/shugoi/challenge_script.rb +24 -0
- data/lib/shugoi/challenge_url.rb +15 -0
- data/lib/shugoi/config.rb +3 -7
- data/lib/shugoi/config_cache.rb +29 -14
- data/lib/shugoi/constants.rb +2 -4
- data/lib/shugoi/core.rb +43 -186
- data/lib/shugoi/csp.rb +3 -21
- data/lib/shugoi/errors.rb +0 -2
- data/lib/shugoi/guard_cache.rb +1 -8
- data/lib/shugoi/guard_injector.rb +31 -0
- data/lib/shugoi/html_store.rb +10 -12
- data/lib/shugoi/key_validator.rb +36 -0
- data/lib/shugoi/locales.rb +15 -9
- data/lib/shugoi/notice.rb +59 -87
- data/lib/shugoi/pow.rb +3 -24
- data/lib/shugoi/rack/middleware.rb +11 -118
- data/lib/shugoi/rails/configuration.rb +4 -7
- data/lib/shugoi/rails/railtie.rb +0 -10
- data/lib/shugoi/rate_limit_formatter.rb +13 -0
- data/lib/shugoi/render_endpoint.rb +24 -0
- data/lib/shugoi/render_handler.rb +0 -22
- data/lib/shugoi/response_processor.rb +36 -0
- data/lib/shugoi/shield_page.rb +82 -0
- data/lib/shugoi/skeleton_generator.rb +25 -30
- data/lib/shugoi/token_signer.rb +0 -15
- data/lib/shugoi/utils.rb +0 -25
- data/lib/shugoi/version.rb +1 -3
- data/lib/shugoi.rb +12 -3
- metadata +44 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89733ca0ebe0286fdb6818ea06a1127dfd6731cabf8a107163fa7df853a4280f
|
|
4
|
+
data.tar.gz: 7b4cb8e61a095cf0ddf6cadd8fc9fd194a81405d697387c657320a6ebbd92df7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26fdbcfc3d51751c68e31a437539d024e036f5afc8569afd7e4174dec5f76b7939edc3236a83152d7f865e15d025e34efc03e9c664f261767499bf667fcb9db1
|
|
7
|
+
data.tar.gz: 930b00eb96a31c6400792d092c5af08c2e2a7b57f578ae0714e153241c73c07d316f30bc3183a504763597891def44da03cfc025f74077bfc00faa9644b186a9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Maintenance
|
|
6
|
+
- Extracted challenge, guard, rate-limit and response responsibilities into focused modules.
|
|
7
|
+
- Added dedicated regression specs and finalized RuboCop validation.
|
|
8
|
+
- Documented the gem architecture and production metadata.
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- Removed dynamic JavaScript evaluation from the split-render bootstrap.
|
|
12
|
+
- Removed `unsafe-eval` from the default Content Security Policy.
|
|
13
|
+
|
|
14
|
+
### Performance
|
|
15
|
+
- Removed Unicode-tag expansion from skeleton responses, reducing transfer size and parse work.
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Bloque les bots, scrapers et clients headless avec :
|
|
|
6
6
|
- **Pre-flight PoW challenge** (307 + tableau ASCII pour curl/view-source, JS de résolution inline pour les navigateurs)
|
|
7
7
|
- **Machine fingerprinting** (guard-detect) + whitelist
|
|
8
8
|
- **Render-grant HMAC** (lié au siteKey + token + IP + TTL) — anti-bypass "token-only"
|
|
9
|
-
- **Split-render** :
|
|
9
|
+
- **Split-render** : bootstrap JavaScript injecté dans la page, remplacé par le contenu réel après vérification
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -59,12 +59,13 @@ run MyApp
|
|
|
59
59
|
```bash
|
|
60
60
|
bundle install
|
|
61
61
|
bundle exec rspec
|
|
62
|
+
bundle exec rake lint
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
## Parité avec le module Node
|
|
65
66
|
|
|
66
67
|
Le flux réplique exactement `shugoi` 0.3.6 (npm) :
|
|
67
68
|
- `core.evaluate` → challenge 307 / `/__sg_challenge` / blocage headless
|
|
68
|
-
- `injectGuardScripts` → token signé +
|
|
69
|
+
- `injectGuardScripts` → token signé + bootstrap sans évaluation dynamique
|
|
69
70
|
- `renderResponseData` → vérification grant + token, service du HTML stocké
|
|
70
71
|
- `verifyRenderGrant` → format `base36(ts):HMAC(secret, "render-grant:siteKey:mid:token:ip:ts")`
|
data/lib/shugoi/api_client.rb
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require "net/http"
|
|
4
2
|
require "uri"
|
|
5
3
|
require "json"
|
|
6
4
|
|
|
7
5
|
module Shugoi
|
|
8
|
-
# Client HTTP vers l'API Shugoi (parité avec fetch dans le module Node).
|
|
9
6
|
class ApiClient
|
|
10
7
|
TIMEOUT = 5.0
|
|
11
8
|
|
|
@@ -14,9 +11,6 @@ module Shugoi
|
|
|
14
11
|
@debug = debug
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
# GET /whitelist?key=...&cb=...&sig=... → { detectionFlags, skipPaths }
|
|
18
|
-
# La siteKey est publique ; la configuration serveur doit prouver la possession
|
|
19
|
-
# du secret de signature (parité Node/PHP, F2/F3).
|
|
20
14
|
def fetch_whitelist(site_key, secret = nil)
|
|
21
15
|
cb = SecureRandom.hex(4)
|
|
22
16
|
params = { key: site_key, cb: cb.to_s }
|
|
@@ -31,18 +25,15 @@ module Shugoi
|
|
|
31
25
|
{ whitelist: [], flags: {}, skip_paths: [] }
|
|
32
26
|
end
|
|
33
27
|
|
|
34
|
-
# GET /guard-detect?key=...&raw=1&cb=...&sig=... → texte du guard
|
|
35
28
|
def fetch_guard_detect(site_key, secret = nil)
|
|
36
29
|
cb = Utils.now_ms
|
|
37
30
|
query = { key: site_key, raw: 1, cb: cb.to_s }
|
|
38
31
|
query[:sig] = Utils.hmac_hex(secret, cb.to_s) if secret && !secret.empty?
|
|
39
|
-
|
|
40
|
-
body
|
|
32
|
+
get_raw("/guard-detect", query)
|
|
41
33
|
rescue StandardError
|
|
42
34
|
nil
|
|
43
35
|
end
|
|
44
36
|
|
|
45
|
-
# POST /event — enregistrement d'un événement (block, headless…)
|
|
46
37
|
def post_event(site_key, reason, machine_id = "")
|
|
47
38
|
payload = { siteKey: site_key, reason: reason, machineId: machine_id }
|
|
48
39
|
post_json("/event", payload)
|
|
@@ -50,16 +41,13 @@ module Shugoi
|
|
|
50
41
|
nil
|
|
51
42
|
end
|
|
52
43
|
|
|
53
|
-
# POST /validate-key — valide la clé
|
|
54
44
|
def validate_key(site_key, secret)
|
|
55
45
|
payload = { siteKey: site_key, secret: secret }
|
|
56
|
-
|
|
57
|
-
json
|
|
46
|
+
post_json("/validate-key", payload)
|
|
58
47
|
rescue StandardError
|
|
59
48
|
{ "valid" => false, "reason" => "network" }
|
|
60
49
|
end
|
|
61
50
|
|
|
62
|
-
# POST /rate-limit-check — vérifie le quota edge par IP (parité core.ts).
|
|
63
51
|
def check_rate_limit(site_key, ip, user_agent = "")
|
|
64
52
|
payload = {
|
|
65
53
|
siteKey: site_key,
|
|
@@ -77,7 +65,8 @@ module Shugoi
|
|
|
77
65
|
def get(path, params)
|
|
78
66
|
uri = build_uri(path, params)
|
|
79
67
|
log("GET #{uri}")
|
|
80
|
-
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
68
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
69
|
+
read_timeout: TIMEOUT) do |http|
|
|
81
70
|
http.get(uri.request_uri, "User-Agent" => "shugoi-ruby/#{VERSION}")
|
|
82
71
|
end
|
|
83
72
|
JSON.parse(res.body)
|
|
@@ -85,27 +74,27 @@ module Shugoi
|
|
|
85
74
|
|
|
86
75
|
def get_raw(path, params)
|
|
87
76
|
uri = build_uri(path, params)
|
|
88
|
-
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
77
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
78
|
+
read_timeout: TIMEOUT) do |http|
|
|
89
79
|
http.get(uri.request_uri, "User-Agent" => "shugoi-ruby/#{VERSION}")
|
|
90
80
|
end
|
|
91
|
-
# Décodage UTF-8 avec remplacement (parité fetch().text() du module Node).
|
|
92
81
|
res.body.to_s.force_encoding(Encoding::UTF_8).scrub("\uFFFD")
|
|
93
82
|
end
|
|
94
83
|
|
|
95
84
|
def post_json(path, payload)
|
|
96
85
|
uri = build_uri(path)
|
|
97
|
-
req = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json",
|
|
86
|
+
req = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json",
|
|
87
|
+
"User-Agent" => "shugoi-ruby/#{VERSION}")
|
|
98
88
|
req.body = JSON.generate(payload)
|
|
99
|
-
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
89
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
|
|
90
|
+
read_timeout: TIMEOUT) do |http|
|
|
100
91
|
http.request(req)
|
|
101
92
|
end
|
|
102
93
|
JSON.parse(res.body)
|
|
103
94
|
end
|
|
104
95
|
|
|
105
96
|
def build_uri(path, params = {})
|
|
106
|
-
|
|
107
|
-
# remplacerait le préfixe /api/v1 → https://shugoi.com/guard-detect (catch-all SPA) !
|
|
108
|
-
uri = URI(@base_url.sub(%r{/\z}, "") + "/" + path.sub(%r{\A/}, ""))
|
|
97
|
+
uri = URI("#{@base_url.sub(%r{/\z}, '')}/#{path.sub(%r{\A/}, '')}")
|
|
109
98
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
110
99
|
uri
|
|
111
100
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Shugoi
|
|
2
|
+
class BotPolicy
|
|
3
|
+
def initialize(config)
|
|
4
|
+
@config = config
|
|
5
|
+
@verifier = config.verify_bots ? BotVerifier.new : nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def trusted?(user_agent, ip)
|
|
9
|
+
return false unless @config.whitelisted_bot?(user_agent)
|
|
10
|
+
return true unless @verifier
|
|
11
|
+
|
|
12
|
+
@verifier.verify(user_agent, ip) == true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/shugoi/bot_verifier.rb
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require "resolv"
|
|
4
2
|
|
|
5
3
|
module Shugoi
|
|
6
|
-
# Vérifie par DNS inverse + forward que les bots whitelistés viennent bien de leurs
|
|
7
|
-
# plages officielles — parité avec verify-bot.ts (module Node). Résultat mis en cache.
|
|
8
4
|
class BotVerifier
|
|
9
5
|
BOT_DOMAINS = [
|
|
10
6
|
{ pattern: /Googlebot|Google-InspectionTool|Storebot-Google/i, suffixes: [".googlebot.com", ".google.com"] },
|
|
@@ -23,9 +19,6 @@ module Shugoi
|
|
|
23
19
|
@mutex = Mutex.new
|
|
24
20
|
end
|
|
25
21
|
|
|
26
|
-
# @param ua [String] User-Agent
|
|
27
|
-
# @param ip [String] IP du client
|
|
28
|
-
# @return [true, false, nil] true/false si UA reconnu, nil sinon
|
|
29
22
|
def verify(ua, ip)
|
|
30
23
|
entry = BOT_DOMAINS.find { |b| b[:pattern].match?(ua) }
|
|
31
24
|
return nil unless entry
|
|
@@ -48,21 +41,19 @@ module Shugoi
|
|
|
48
41
|
|
|
49
42
|
private
|
|
50
43
|
|
|
51
|
-
# PTR de l'IP → nom se terminant par un suffixe attendu, puis forward vérifié.
|
|
52
44
|
def reverse_forward_match?(ip, suffixes)
|
|
53
45
|
names = Resolv.getnames(ip)
|
|
54
46
|
name = names.find { |n| suffixes.any? { |s| n.downcase.end_with?(s) } }
|
|
55
47
|
return false unless name
|
|
56
48
|
addresses = Resolv.getaddresses(name)
|
|
57
49
|
addresses.include?(ip)
|
|
58
|
-
rescue
|
|
50
|
+
rescue StandardError
|
|
59
51
|
false
|
|
60
52
|
end
|
|
61
53
|
|
|
62
54
|
def prune
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
55
|
+
return unless @cache.size >= MAX_ENTRIES
|
|
56
|
+
@cache.delete(@cache.keys.min_by { |k| @cache[k][:at] })
|
|
66
57
|
end
|
|
67
58
|
end
|
|
68
59
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Shugoi
|
|
2
|
+
class ChallengeLimiter
|
|
3
|
+
LIMIT = 60
|
|
4
|
+
WINDOW_MS = 60_000
|
|
5
|
+
MAX_BLOCK_MS = 15 * 60 * 1000
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@limits = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def allow?(ip)
|
|
13
|
+
return true if ip.to_s.empty? || ip == "unknown"
|
|
14
|
+
|
|
15
|
+
now = Utils.now_ms
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
entry = @limits[ip]
|
|
18
|
+
if entry.nil? || now - entry[:window_start] >= WINDOW_MS
|
|
19
|
+
@limits[ip] = { count: 1, window_start: now, blocked_until: 0 }
|
|
20
|
+
next true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
entry[:count] += 1
|
|
24
|
+
next false if entry[:blocked_until] > now
|
|
25
|
+
|
|
26
|
+
if entry[:count] > LIMIT
|
|
27
|
+
exponent = [entry[:count] - LIMIT, 10].min
|
|
28
|
+
backoff = [60_000 * (2**exponent), MAX_BLOCK_MS].min
|
|
29
|
+
entry[:blocked_until] = now + backoff
|
|
30
|
+
entry[:count] = 0
|
|
31
|
+
next false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Shugoi
|
|
2
|
+
class ChallengePath
|
|
3
|
+
def self.sanitize(path)
|
|
4
|
+
return '/' if path.to_s.empty?
|
|
5
|
+
return '/' unless path.start_with?('/')
|
|
6
|
+
return '/' if path.start_with?('//') || path.include?('\\')
|
|
7
|
+
return '/' if path.each_codepoint.any? { |codepoint| codepoint < 0x20 || codepoint == 0x7f }
|
|
8
|
+
|
|
9
|
+
path
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Shugoi
|
|
2
|
+
class ChallengeScript
|
|
3
|
+
def self.render(difficulty)
|
|
4
|
+
<<~JS
|
|
5
|
+
(function(){
|
|
6
|
+
var P=new URLSearchParams(location.search);
|
|
7
|
+
var salt=P.get('salt')||'', ts=P.get('ts')||'', nonce=P.get('nonce')||'', diff=parseInt(P.get('diff')||'#{difficulty}',10), path=P.get('path')||'/';
|
|
8
|
+
if(path.charAt(0)!=='/'||path.charAt(1)==='/'||path.indexOf('\\\\')>=0)path='/';
|
|
9
|
+
var enc=new TextEncoder();
|
|
10
|
+
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}l+=(b&8)?0:(b&4)?1:(b&2)?2:3;break}return l}
|
|
11
|
+
var n=0;
|
|
12
|
+
function step(){
|
|
13
|
+
crypto.subtle.digest('SHA-256',enc.encode(salt+':'+n.toString(16))).then(function(buf){
|
|
14
|
+
var h=Array.from(new Uint8Array(buf)).map(function(v){return v.toString(16).padStart(2,'0')}).join('');
|
|
15
|
+
if(bits(h)>=diff){var base=path;var q=(base.indexOf('?')>=0?'&':'?')+'sg_proof='+ts+':'+nonce+':'+n.toString(16);location.replace(base+q)}
|
|
16
|
+
else{n++;if(n<300000)step()}
|
|
17
|
+
}).catch(function(){location.reload()});
|
|
18
|
+
}
|
|
19
|
+
step();
|
|
20
|
+
})();
|
|
21
|
+
JS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Shugoi
|
|
2
|
+
class ChallengeUrl
|
|
3
|
+
def self.build(secret:, difficulty:, context:)
|
|
4
|
+
timestamp = Utils.now_sec
|
|
5
|
+
nonce = SecureRandom.hex(8)
|
|
6
|
+
salt = Utils.hmac_hex(secret, "#{timestamp}:#{nonce}")
|
|
7
|
+
prefix = context[:forwarded_prefix].to_s
|
|
8
|
+
prefix = "" if prefix == "/"
|
|
9
|
+
prefix = prefix.sub(%r{/\z}, "") unless prefix.empty?
|
|
10
|
+
path = ChallengePath.sanitize(context[:path].to_s)
|
|
11
|
+
query = "ts=#{timestamp}&salt=#{salt}&nonce=#{nonce}&diff=#{difficulty}"
|
|
12
|
+
"#{prefix}/__sg_challenge?#{query}&path=#{URI.encode_www_form_component(prefix + path)}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/shugoi/config.rb
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
module Shugoi
|
|
4
|
-
# Options du middleware — parité avec ShugoiCoreOptions (module Node).
|
|
5
2
|
class Config
|
|
6
3
|
DEFAULTS = {
|
|
7
4
|
site_key: nil,
|
|
@@ -23,7 +20,6 @@ module Shugoi
|
|
|
23
20
|
split_render: true,
|
|
24
21
|
multi_process: false,
|
|
25
22
|
verify_bots: true,
|
|
26
|
-
# Parité module Node : difficulté PoW 14 par défaut (configurable), TTL 60 s.
|
|
27
23
|
pow_difficulty: 14,
|
|
28
24
|
pow_ttl_ms: 60_000
|
|
29
25
|
}.freeze
|
|
@@ -57,15 +53,15 @@ module Shugoi
|
|
|
57
53
|
def pow_difficulty = @options[:pow_difficulty]
|
|
58
54
|
def pow_ttl_ms = @options[:pow_ttl_ms]
|
|
59
55
|
|
|
60
|
-
def
|
|
56
|
+
def allowlisted?(path)
|
|
61
57
|
allowlist.any? { |p| path == p || path.start_with?("#{p}/") }
|
|
62
58
|
end
|
|
63
59
|
|
|
64
|
-
def
|
|
60
|
+
def whitelisted_bot?(ua)
|
|
65
61
|
bot_whitelist.any? { |p| p.match?(ua) }
|
|
66
62
|
end
|
|
67
63
|
|
|
68
|
-
def
|
|
64
|
+
def headless?(ua)
|
|
69
65
|
headless_patterns.any? { |p| p.match?(ua) }
|
|
70
66
|
end
|
|
71
67
|
|
data/lib/shugoi/config_cache.rb
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
module Shugoi
|
|
4
|
-
# Cache mémoire de la config (whitelist + flags + skipPaths).
|
|
5
|
-
# Parité avec _configCache (render.ts) : TTL 30s + stale-refresh.
|
|
6
2
|
class ConfigCache
|
|
7
3
|
TTL_MS = 30_000
|
|
8
4
|
STALE_MAX_MS = 600_000
|
|
@@ -15,16 +11,29 @@ module Shugoi
|
|
|
15
11
|
@flags = {}
|
|
16
12
|
@skip_paths = []
|
|
17
13
|
@fetched_at = 0
|
|
14
|
+
@refreshing = false
|
|
18
15
|
end
|
|
19
16
|
|
|
20
17
|
def fetch(site_key)
|
|
18
|
+
now = Utils.now_ms
|
|
19
|
+
refresh_now = false
|
|
20
|
+
refresh_async = false
|
|
21
|
+
|
|
21
22
|
@mutex.synchronize do
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
age = now - @fetched_at
|
|
24
|
+
refresh_now = @fetched_at.zero? || age > STALE_MAX_MS
|
|
25
|
+
refresh_async = !refresh_now && age > TTL_MS && !@refreshing
|
|
26
|
+
@refreshing = true if refresh_now || refresh_async
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if refresh_now
|
|
30
|
+
refresh(site_key)
|
|
31
|
+
elsif refresh_async
|
|
32
|
+
Thread.new { refresh(site_key) }.abort_on_exception = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
{ whitelist: @whitelist.dup, flags: @flags.dup, skip_paths: @skip_paths.dup }
|
|
28
37
|
end
|
|
29
38
|
end
|
|
30
39
|
|
|
@@ -32,10 +41,16 @@ module Shugoi
|
|
|
32
41
|
|
|
33
42
|
def refresh(site_key)
|
|
34
43
|
data = @api_client.fetch_whitelist(site_key, @signing_secret)
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@whitelist = data[:whitelist]
|
|
46
|
+
@flags = data[:flags]
|
|
47
|
+
@skip_paths = data[:skip_paths]
|
|
48
|
+
@fetched_at = Utils.now_ms
|
|
49
|
+
@refreshing = false
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError
|
|
52
|
+
@mutex.synchronize { @refreshing = false }
|
|
53
|
+
raise
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
end
|
data/lib/shugoi/constants.rb
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
module Shugoi
|
|
4
2
|
BLOCK_PAGE = [
|
|
5
3
|
"+---------------------------------------------+",
|
|
@@ -13,10 +11,10 @@ module Shugoi
|
|
|
13
11
|
"| |",
|
|
14
12
|
"| - web: https://shugoi.com - |",
|
|
15
13
|
"+---------------------------------------------+"
|
|
16
|
-
].join("\n")
|
|
14
|
+
].join("\n") << "\n"
|
|
17
15
|
|
|
18
16
|
DEFAULT_HEADLESS_PATTERNS = [
|
|
19
|
-
/^curl/i, /^wget/i, /^python/i, /^Go-http-client/i,
|
|
17
|
+
/^curl/i, /^wget/i, /^python/i, /^Go-http-client/i, %r{^Java/},
|
|
20
18
|
/HTTPie/i, /^node-fetch/i, /axios/i, /^okhttp/i, /^scrapy/i,
|
|
21
19
|
/PowerShell/i, /WinHttp/i
|
|
22
20
|
].freeze
|