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: a1913d93cd52d599d33f7217bb3714fbf67c82d08a888a3025cc30e51c51e438
4
- data.tar.gz: 6e06e420e625a069132ae89a3ef733c1b2c246878e771e3b459ea0cdc351d14e
3
+ metadata.gz: 2e582326d0a139a9407ccd31a0bbb822d99d51a7040093d22427d563997dbea8
4
+ data.tar.gz: 68107c7fc6a9aa2d8e3e079e401708c740e3cf6ee840b7be6cc4f7c85e8b64df
5
5
  SHA512:
6
- metadata.gz: 11504f2622783e4ca4bfb06b981df28260c1903e04a8900fe1797d34cd05bc29bcf3b2a1a6b9c93b85ae0f3a4639ff115699d4a548f6912b839bee62147e595c
7
- data.tar.gz: 229ed02d6173b651ffd3cbef26e6444a974a14a1e9e3390d76567774a2385ad38cb4e1c289a78dda2c66f57d0e6a54e375506c9826cdbd7df7f799a7b96f03ff
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
@@ -1,3 +1,3 @@
1
1
  module BotChallengePage
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
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.0
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-03-19 00:00:00.000000000 Z
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