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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e582326d0a139a9407ccd31a0bbb822d99d51a7040093d22427d563997dbea8
4
- data.tar.gz: 68107c7fc6a9aa2d8e3e079e401708c740e3cf6ee840b7be6cc4f7c85e8b64df
3
+ metadata.gz: 1f123fae1f11aef11705e08404a9275bf61e9657cac5bc2833145494a26cbb10
4
+ data.tar.gz: 9ea4ba0896da4e3dc7ca4307fff1c18cc2f0b33a7c792d11f3ed926559f183a8
5
5
  SHA512:
6
- metadata.gz: 3748c684eeca9e6e66b50fa5e0e5fe4b97c328862799a9dee8fc19632974df5c4489e076eede3d67f0ca8d6f5dc16476ae682d938be015606837c232866256ce
7
- data.tar.gz: 2b0bf202ef9c4751a07d0acebeaedd722fd4de501b0a0ae94b004b70b03fb77682cd472aa7e1d3a34ff36810ce3b532662751f3cc79c5453c5fa673977afd0db
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, following lehigh's lead, although we use a smaller
70
- # subnet: /24 for IPv4, and /72 for IPv6
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}/72").to_string
74
+ IPAddr.new("#{req.ip}/64").to_string
75
75
  else
76
- IPAddr.new("#{req.ip}/24").to_string
76
+ IPAddr.new("#{req.ip}/16").to_string
77
77
  end
78
78
  rescue IPAddr::InvalidAddressError
79
79
  req.ip
@@ -1,3 +1,3 @@
1
1
  module BotChallengePage
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
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.1
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-07 00:00:00.000000000 Z
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