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

Class Method Details

.available?(env: ENV, stdin: $stdin, stdout: $stdout) ⇒ Boolean

Returns:

  • (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

Returns:

  • (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.message}. 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
    cookies = ChromeAuth.login!(errput: errput, input: input,
                                 openURL: ChromeAuth::REFRESH_URL)
    cookies.each { |k, v| $cookies[k] = v unless v.to_s.empty? }
    !cookies.empty?
rescue StandardError => e
    errput.puts "(Chrome auto-recovery failed: #{e.class}: #{e.message}. 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