Module: Request::InteractiveCloudflareRecovery
- Defined in:
- lib/Request.rb
Overview
Interactive Cloudflare recovery: when running on a developer’s own machine (i.e. there is a real TTY and no CI marker env var), instead of just raising CloudflareBlockedError we can open Medium in the user’s default browser, let them clear the challenge by hand, and retry the request once. CI environments still raise immediately.
Constant Summary collapse
- CI_ENV_VARS =
Common CI env vars. If any of these is set to a non-empty, non-“false” value, we assume non-interactive.
%w[CI GITHUB_ACTIONS GITLAB_CI CIRCLECI JENKINS_URL BUILDKITE TF_BUILD TRAVIS APPVEYOR].freeze
- DISABLE_ENV_VAR =
Explicit opt-out for users who want the old raise-and-exit behavior even on a TTY.
'MEDIUM_NO_AUTO_BROWSER'.freeze
Class Method Summary collapse
- .available?(env: ENV, stdin: $stdin, stdout: $stdout) ⇒ Boolean
- .inCIEnvironment?(env = ENV) ⇒ Boolean
-
.openCommand(url, hostOS: RbConfig::CONFIG['host_os']) ⇒ Object
Build the platform-appropriate command for opening a URL in the default browser.
- .openInBrowser(url, errput: $stderr) ⇒ Object
-
.run(url, errput: $stderr, input: $stdin, autoOpen: true) ⇒ Object
Run the interactive recovery flow.
- .runChromeFlow(url, errput:, input:) ⇒ Object
- .runDefaultBrowserFlow(url, errput:, input:, autoOpen:) ⇒ Object
Class Method Details
.available?(env: ENV, stdin: $stdin, stdout: $stdout) ⇒ Boolean
73 74 75 76 77 78 79 80 |
# File 'lib/Request.rb', line 73 def available?(env: ENV, stdin: $stdin, stdout: $stdout) return false if env[DISABLE_ENV_VAR].to_s == '1' return false if inCIEnvironment?(env) stdin.tty? && stdout.tty? rescue StandardError # Some test stdio doubles don't implement .tty? — treat as non-interactive. false end |
.inCIEnvironment?(env = ENV) ⇒ Boolean
82 83 84 85 86 87 |
# File 'lib/Request.rb', line 82 def inCIEnvironment?(env = ENV) CI_ENV_VARS.any? do |key| value = env[key].to_s !value.empty? && value.downcase != 'false' && value != '0' end end |
.openCommand(url, hostOS: RbConfig::CONFIG['host_os']) ⇒ Object
Build the platform-appropriate command for opening a URL in the default browser. Returned as an array so callers can spawn / system without going through a shell.
92 93 94 95 96 97 98 |
# File 'lib/Request.rb', line 92 def openCommand(url, hostOS: RbConfig::CONFIG['host_os']) case hostOS when /darwin/ then ['open', url] when /mswin|mingw|cygwin/ then ['cmd', '/c', 'start', '', url] else ['xdg-open', url] end end |
.openInBrowser(url, errput: $stderr) ⇒ Object
100 101 102 103 104 |
# File 'lib/Request.rb', line 100 def openInBrowser(url, errput: $stderr) spawn(*openCommand(url), out: File::NULL, err: File::NULL) rescue Errno::ENOENT, StandardError => e errput.puts "(Couldn't auto-open browser — #{e.class}: #{e.}. Open #{url} manually.)" end |
.run(url, errput: $stderr, input: $stdin, autoOpen: true) ⇒ Object
Run the interactive recovery flow. Returns true if the user cleared the challenge (and, when Chrome is available, we successfully refreshed cookies); false if they pressed Ctrl-D (EOF) or otherwise gave up.
Two paths:
1. ChromeAuth available → drive Chrome via ferrum; on success
sid/uid/cf_clearance/_cfuvid land in $cookies and the cache.
2. Otherwise → legacy fallback: open default browser, ask the
user to clear the challenge by hand, retry without new cookies.
116 117 118 119 120 121 122 |
# File 'lib/Request.rb', line 116 def run(url, errput: $stderr, input: $stdin, autoOpen: true) if ChromeAuth.available? return runChromeFlow(url, errput: errput, input: input) end runDefaultBrowserFlow(url, errput: errput, input: input, autoOpen: autoOpen) end |
.runChromeFlow(url, errput:, input:) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/Request.rb', line 124 def runChromeFlow(url, errput:, input:) errput.puts <<~MSG ────────────────────────────────────────────────────────────────────── ⚠ Cloudflare bot challenge detected at #{url}. Opening Chrome so you can clear it (and refresh login if needed). ────────────────────────────────────────────────────────────────────── MSG = ChromeAuth.login!(errput: errput, input: input, openURL: ChromeAuth::REFRESH_URL) .each { |k, v| $cookies[k] = v unless v.to_s.empty? } !.empty? rescue StandardError => e errput.puts "(Chrome auto-recovery failed: #{e.class}: #{e.}. Falling back to default browser.)" runDefaultBrowserFlow(url, errput: errput, input: input, autoOpen: true) end |
.runDefaultBrowserFlow(url, errput:, input:, autoOpen:) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/Request.rb', line 142 def runDefaultBrowserFlow(url, errput:, input:, autoOpen:) errput.puts <<~MSG ────────────────────────────────────────────────────────────────────── ⚠ Cloudflare bot challenge detected at #{url}. Since this looks like an interactive run, you can clear the challenge in your browser: 1. A browser window will open at https://medium.com. 2. Complete the "Just a moment…" / CAPTCHA challenge there. 3. Come back here and press Enter to retry. (Install Google Chrome to enable auto-cookie capture next time.) (To disable this prompt and just fail fast, set #{DISABLE_ENV_VAR}=1.) ────────────────────────────────────────────────────────────────────── MSG openInBrowser('https://medium.com', errput: errput) if autoOpen errput.print 'Press Enter once the challenge is cleared (Ctrl-D to give up)… ' line = input.gets errput.puts !line.nil? end |