bot_challenge_page 0.3.0 → 0.3.1
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e582326d0a139a9407ccd31a0bbb822d99d51a7040093d22427d563997dbea8
|
4
|
+
data.tar.gz: 68107c7fc6a9aa2d8e3e079e401708c740e3cf6ee840b7be6cc4f7c85e8b64df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3748c684eeca9e6e66b50fa5e0e5fe4b97c328862799a9dee8fc19632974df5c4489e076eede3d67f0ca8d6f5dc16476ae682d938be015606837c232866256ce
|
7
|
+
data.tar.gz: 2b0bf202ef9c4751a07d0acebeaedd722fd4de501b0a0ae94b004b70b03fb77682cd472aa7e1d3a34ff36810ce3b532662751f3cc79c5453c5fa673977afd0db
|
data/README.md
CHANGED
@@ -169,6 +169,8 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
169
169
|
|
170
170
|
* Joe's [similar plugin for drupal](https://drupal.org/project/turnstile_protect)
|
171
171
|
|
172
|
+
* Joe's [similar plugin for traefik reverse-proxy](https://github.com/libops/captcha-protect)
|
173
|
+
|
172
174
|
* [Similar feature built into PHP VuFind app](https://github.com/vufind-org/vufind/pull/4079)
|
173
175
|
|
174
176
|
* [My own blog post about this approach](https://bibwild.wordpress.com/2025/01/16/using-cloudflare-turnstile-to-protect-certain-pages-on-a-rails-app/).
|
@@ -71,9 +71,9 @@ module BotChallengePage
|
|
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}/24").to_string
|
75
|
-
else
|
76
74
|
IPAddr.new("#{req.ip}/72").to_string
|
75
|
+
else
|
76
|
+
IPAddr.new("#{req.ip}/24").to_string
|
77
77
|
end
|
78
78
|
rescue IPAddr::InvalidAddressError
|
79
79
|
req.ip
|
@@ -0,0 +1,90 @@
|
|
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
|
File without changes
|
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.3.
|
4
|
+
version: 0.3.1
|
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-
|
11
|
+
date: 2025-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: appraisal
|
@@ -156,6 +156,8 @@ 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
|
+
- app/models/bot_challenge_page/test.html
|
159
161
|
- app/views/bot_challenge_page/_local_turnstile_script_tag.html.erb
|
160
162
|
- app/views/bot_challenge_page/_turnstile_widget_placeholder.html.erb
|
161
163
|
- app/views/bot_challenge_page/bot_challenge_page/challenge.html.erb
|