-
Notifications
You must be signed in to change notification settings - Fork 0
Implement bot detection and verification #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| class TurnstileController < ApplicationController | ||
| before_action :validate_cloudflare_turnstile, only: :verify | ||
|
|
||
| rescue_from RailsCloudflareTurnstile::Forbidden, with: :handle_forbidden | ||
|
|
||
| def show | ||
| @return_to = params[:return_to].presence || root_path | ||
| end | ||
|
|
||
| def verify | ||
| session[:passed_turnstile] = true | ||
| redirect_to safe_return_path | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # Handles Turnstile rejecting token submission due to invalid token, network issue, etc. | ||
| def handle_forbidden | ||
| flash.now[:error] = "We couldn't complete the verification. Please try again." | ||
| render :show, status: :unprocessable_entity | ||
| end | ||
|
|
||
| # Returns a safe path to redirect to after Turnstile verification. Valid paths should begin with | ||
| # a single slash. Falls back to root_path if the provided path is invalid. | ||
| def safe_return_path | ||
JPrevost marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return_to = params[:return_to].to_s | ||
| return root_path if return_to.blank? | ||
| return root_path if return_to.start_with?('//') | ||
| return return_to if return_to.start_with?('/') | ||
|
|
||
| root_path | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| class BotDetector | ||
| # Returns true if the request appears to be a bot according to crawler_detect. | ||
| def self.bot?(request) | ||
| ua = request.user_agent.to_s | ||
| detector = CrawlerDetect.new(ua) | ||
| detector.is_crawler? | ||
| rescue StandardError => e | ||
| Rails.logger.debug("BotDetector: crawler_detect failed for UA '#{ua}': #{e.message}") | ||
| false | ||
| end | ||
|
|
||
| # Returns true when the request appears to be performing crawling behavior that we | ||
| # want to challenge. For our initial approach, treat requests to the search results | ||
| # endpoint as subject to challenge if they're flagged as bots. | ||
| def self.should_challenge?(request) | ||
| return false unless bot?(request) | ||
|
|
||
| # Basic rule: crawling search results or record pages triggers a challenge. | ||
| # /results is the search results page and /record is the full record view. | ||
| # This keeps the rule simple and conservative. | ||
| path = request.path.to_s | ||
| return true if path.start_with?('/results') || path.start_with?('/record') | ||
|
|
||
| false | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ | |
| # | ||
| class Feature | ||
| # List of all valid features in the application | ||
| VALID_FEATURES = %i[geodata boolean_picker oa_always simulate_search_latency tab_primo_all tab_timdex_all | ||
| VALID_FEATURES = %i[bot_detection geodata boolean_picker oa_always simulate_search_latency tab_primo_all tab_timdex_all | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| tab_timdex_alma record_link timdex_fulltext].freeze | ||
|
|
||
| # Check if a feature is enabled by name | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <%= cloudflare_turnstile_script_tag %> | ||
|
|
||
| <section class="turnstile-challenge"> | ||
| <div class="turnstile-challenge__inner"> | ||
| <h1>Verify you're human</h1> | ||
| <p> | ||
| Please complete this verification to continue. | ||
| </p> | ||
|
|
||
| <%= form_with url: turnstile_verify_path, method: :post, local: true do %> | ||
| <%= hidden_field_tag :return_to, @return_to %> | ||
|
|
||
| <div class="turnstile-widget"> | ||
| <%= cloudflare_turnstile(action: 'search') %> | ||
| </div> | ||
|
|
||
| <div class="turnstile-challenge__actions"> | ||
| <%= submit_tag 'Submit', class: 'btn button-primary' %> | ||
| </div> | ||
| <% end %> | ||
| </div> | ||
| </section> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Explicitly require Feature model to check if bot detection is enabled | ||
| require Rails.root.join('app/models/feature') | ||
|
|
||
| module TurnstileConfig | ||
| module_function | ||
|
|
||
| def apply | ||
| RailsCloudflareTurnstile.reset_configuration! | ||
| enabled = bot_detection_enabled? | ||
| enabled = false if Rails.env.test? | ||
|
|
||
| RailsCloudflareTurnstile.configure do |config| | ||
| config.site_key = ENV['TURNSTILE_SITEKEY'] | ||
| config.secret_key = ENV['TURNSTILE_SECRET'] | ||
| config.enabled = enabled | ||
| config.fail_open = !enabled | ||
| config.mock_enabled = Rails.env.test? | ||
| end | ||
| end | ||
|
|
||
| def bot_detection_enabled? | ||
JPrevost marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return false unless Feature.enabled?(:bot_detection) | ||
|
|
||
| # Check that required env is present | ||
| sitekey = ENV.fetch('TURNSTILE_SITEKEY', nil) | ||
| secret = ENV.fetch('TURNSTILE_SECRET', nil) | ||
|
|
||
| if sitekey.blank? || secret.blank? | ||
| Rails.logger.error('Bot detection enabled but missing TURNSTILE_SITEKEY or TURNSTILE_SECRET') | ||
| Sentry.capture_message('Bot detection misconfigured: missing Turnstile credentials', level: :error) | ||
| return false | ||
| end | ||
|
|
||
| true | ||
| end | ||
| end | ||
|
|
||
| TurnstileConfig.apply | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| require 'test_helper' | ||
| require 'climate_control' | ||
|
|
||
| class TurnstileControllerTest < ActionDispatch::IntegrationTest | ||
| def with_bot_detection_enabled | ||
| ClimateControl.modify(FEATURE_BOT_DETECTION: 'true') do | ||
| TurnstileConfig.apply | ||
| yield | ||
| ensure | ||
| TurnstileConfig.apply | ||
| end | ||
| end | ||
|
|
||
| test 'show renders when bot detection is enabled' do | ||
| with_bot_detection_enabled do | ||
| get turnstile_path | ||
| assert_response :success | ||
| end | ||
| end | ||
|
|
||
| test 'verify sets session and redirects back to search' do | ||
| with_bot_detection_enabled do | ||
| post turnstile_verify_path, params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=ocean' } | ||
|
|
||
| assert_redirected_to '/results?q=ocean' | ||
| assert session[:passed_turnstile] | ||
| end | ||
| end | ||
|
|
||
| test 'verify re-renders on failed validation' do | ||
| with_bot_detection_enabled do | ||
| post turnstile_verify_path | ||
|
|
||
| assert_response :unprocessable_entity | ||
| assert_match "We couldn't complete the verification", response.body | ||
| refute session[:passed_turnstile] | ||
| end | ||
| end | ||
|
|
||
| test 'verify falls back to root_path for invalid return_to' do | ||
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| with_bot_detection_enabled do | ||
| post turnstile_verify_path, params: { 'cf-turnstile-response' => 'mocked', return_to: 'foo' } | ||
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert_redirected_to root_path | ||
| assert session[:passed_turnstile] | ||
| end | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.