bot_challenge_page 0.3.1 → 0.4.0
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/README.md +1 -1
- data/app/models/bot_challenge_page/config.rb +4 -4
- data/lib/bot_challenge_page/version.rb +1 -1
- metadata +2 -3
- data/app/models/bot_challenge_page/simple_pow1.rb +0 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f123fae1f11aef11705e08404a9275bf61e9657cac5bc2833145494a26cbb10
|
4
|
+
data.tar.gz: 9ea4ba0896da4e3dc7ca4307fff1c18cc2f0b33a7c792d11f3ed926559f183a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e5ef5eaedf5c9705270da5b55ac3c0f8cf225b22ba09af3bb0782afcbc12ade3bc02b8c642ffaae14ca0c401a5858344c404c144d2094cf9f372f23ae5189c2
|
7
|
+
data.tar.gz: e65401e9d3a5b8379fad737842138312d5f3d3f003e59123246cbb0c0af9de932058736f07ba4b3f161eb39bdbe228e941635f63e861a7dcb0e465a1d7e6c272
|
data/README.md
CHANGED
@@ -120,7 +120,7 @@ Rails.application.config.to_prepare do
|
|
120
120
|
BotChallengePage::BotChallengePageController.bot_challenge_config.rate_limit_period = 36.hour
|
121
121
|
BotChallengePage::BotChallengePageController.bot_challenge_config.rate_limit_count = 3
|
122
122
|
|
123
|
-
BotChallengePage::BotChallengePageController.allow_exempt = ->(controller) {
|
123
|
+
BotChallengePage::BotChallengePageController.allow_exempt = ->(controller, config) {
|
124
124
|
# Excempt any Catalog #facet or #range_limit action that looks like an ajax/fetch request, the # challenge isn't going to work there, we just exempt it.
|
125
125
|
#
|
126
126
|
# sec-fetch-dest is set to 'empty' by browser on fetch requests, to limit us further;
|
@@ -66,14 +66,14 @@ module BotChallengePage
|
|
66
66
|
attribute :after_blocked, default: ->(bot_detect_class) {}
|
67
67
|
|
68
68
|
|
69
|
-
# rate limit per subnet,
|
70
|
-
# subnet: /
|
69
|
+
# rate limit per subnet, follow lehigh's lead with
|
70
|
+
# subnet: /16 for IPv4 (x.y.*.*), and /64 for IPv6 (about the same size subnet for better or worse)
|
71
71
|
# https://git.drupalcode.org/project/turnstile_protect/-/blob/0dae9f95d48f9d8cae5a8e61e767c69f64490983/src/EventSubscriber/Challenge.php#L140-151
|
72
72
|
attribute :rate_limit_discriminator, default: (lambda do |req, config|
|
73
73
|
if req.ip.index(":") # ipv6
|
74
|
-
IPAddr.new("#{req.ip}/
|
74
|
+
IPAddr.new("#{req.ip}/64").to_string
|
75
75
|
else
|
76
|
-
IPAddr.new("#{req.ip}/
|
76
|
+
IPAddr.new("#{req.ip}/16").to_string
|
77
77
|
end
|
78
78
|
rescue IPAddr::InvalidAddressError
|
79
79
|
req.ip
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bot_challenge_page
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Rochkind
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: appraisal
|
@@ -156,7 +156,6 @@ files:
|
|
156
156
|
- app/controllers/concerns/bot_challenge_page/enforce_filter.rb
|
157
157
|
- app/controllers/concerns/bot_challenge_page/rack_attack_init.rb
|
158
158
|
- app/models/bot_challenge_page/config.rb
|
159
|
-
- app/models/bot_challenge_page/simple_pow1.rb
|
160
159
|
- app/models/bot_challenge_page/test.html
|
161
160
|
- app/views/bot_challenge_page/_local_turnstile_script_tag.html.erb
|
162
161
|
- app/views/bot_challenge_page/_turnstile_widget_placeholder.html.erb
|
@@ -1,90 +0,0 @@
|
|
1
|
-
require 'digest'
|
2
|
-
require "base64"
|
3
|
-
|
4
|
-
module BotChallengePage
|
5
|
-
# A simple proof-of-work algorithm, that we can also do in javascript
|
6
|
-
#
|
7
|
-
# ## Algorithm
|
8
|
-
#
|
9
|
-
# We calculate a deterministic "challenge" based on a secret key (salt?), current time period,
|
10
|
-
# and the specific client request characteristics (prob just client IP).
|
11
|
-
#
|
12
|
-
# The client has to find a prefix than when prepended to the challenge yields a Sha256 hash
|
13
|
-
# that begins with a certain number of zeroes in the hex representtion. The number of zeroes is the "difficulty".
|
14
|
-
# Each zero in hex rep is 4 bits.
|
15
|
-
#
|
16
|
-
# They send the prefix back to us as a solution, and we confirm that when prefixed to
|
17
|
-
# our challenge, and hashed, it has the required number of leading zeroes.
|
18
|
-
#
|
19
|
-
# (TODO: Leading zeroes in a hex represnetation or what?)
|
20
|
-
class SimplePow1
|
21
|
-
# how long is a challenge good for, it will really be good for somewhere between this and 2x this,
|
22
|
-
# since we always try previous challenge to avoid race condition on switch
|
23
|
-
CHALLENGE_PERIOD = 6.minutes
|
24
|
-
|
25
|
-
# how many leading 0 *BITS* -- and time varies a LOT and expands RAPIDLY when we add, we dont' totally knokw what we're doing
|
26
|
-
DEFAULT_DIFFICULTY = 18
|
27
|
-
|
28
|
-
DEFAULT_SECRET = ActiveSupport::KeyGenerator.new(Rails.application.config.secret_key_base).generate_key("BotChallengePage::SimplePow1")
|
29
|
-
|
30
|
-
attr_reader :client_id, :difficulty
|
31
|
-
|
32
|
-
def initialize(client_id:, secret: DEFAULT_SECRET, difficulty: DEFAULT_DIFFICULTY)
|
33
|
-
@client_id = client_id # usually client ip
|
34
|
-
@difficulty = difficulty
|
35
|
-
@secret = secret
|
36
|
-
end
|
37
|
-
|
38
|
-
# challenge is determinsitic based on our secret, the current time, and the client_id
|
39
|
-
def challenge(for_time: Time.now.utc)
|
40
|
-
period_normalized_time = for_time - (for_time.to_i % CHALLENGE_PERIOD)
|
41
|
-
|
42
|
-
Digest::SHA256.hexdigest "#{period_normalized_time.to_s}_#{client_id.to_s}_#{@secret.to_s}"
|
43
|
-
end
|
44
|
-
|
45
|
-
def challenge_for_last_period
|
46
|
-
challenge(for_time: Time.now.utc - CHALLENGE_PERIOD)
|
47
|
-
end
|
48
|
-
|
49
|
-
def challenge_params
|
50
|
-
{
|
51
|
-
challenge: challenge,
|
52
|
-
difficulty: difficulty
|
53
|
-
}
|
54
|
-
end
|
55
|
-
|
56
|
-
# Check solution against current challenge, AND against the previous period's challenge,
|
57
|
-
# in case we just had a race condition, meaning our time goid is actually
|
58
|
-
# min CHALLENGE_PERIOD and max 2 * CHALLENGE_PERIOD
|
59
|
-
#
|
60
|
-
# @param solution [String] *Base64-encoded data*, that when prefixed to the challenge,
|
61
|
-
# results in a sha256 digest with `difficulty` leading 0 bits.
|
62
|
-
#
|
63
|
-
def verify_solution(solution)
|
64
|
-
solution = Base64.decode64(solution)
|
65
|
-
|
66
|
-
verify_solution_for_challenge(solution, challenge) ||
|
67
|
-
verify_solution_for_challenge(solution, challenge(for_time: Time.now.utc - CHALLENGE_PERIOD))
|
68
|
-
end
|
69
|
-
|
70
|
-
# @param solution [String] actual data, **not** base64 encoded
|
71
|
-
#
|
72
|
-
def verify_solution_for_challenge(aSolution, aChallenge)
|
73
|
-
# there's prob a more efficient mathematical way to do this wihtout converting
|
74
|
-
# to hex string, but this is what we've got.
|
75
|
-
bindigest = Digest::SHA256.digest(aSolution + aChallenge)
|
76
|
-
|
77
|
-
# hopefully we are not going to have a problem with endian-ness here. :(
|
78
|
-
|
79
|
-
bytes_required = (difficulty / 8) + 1
|
80
|
-
prefix_bytes = bindigest.byteslice(0, bytes_required).bytes
|
81
|
-
prefix_bits = prefix_bytes.collect do |byte|
|
82
|
-
reversed_bits = byte.digits(2)
|
83
|
-
reversed_bits.fill(0, reversed_bits.length..7).reverse
|
84
|
-
end.compact.join.slice(0, difficulty)
|
85
|
-
|
86
|
-
prefix_bits == ("0" * difficulty)
|
87
|
-
end
|
88
|
-
|
89
|
-
end
|
90
|
-
end
|