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)


69
70
71
72
73
74
75
76
# File 'lib/Request.rb', line 69

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)


78
79
80
81
82
83
# File 'lib/Request.rb', line 78

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.



88
89
90
91
92
93
94
# File 'lib/Request.rb', line 88

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



96
97
98
99
100
# File 'lib/Request.rb', line 96

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 confirmed they cleared the challenge, false if they pressed Ctrl-D (EOF) or otherwise gave up.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/Request.rb', line 105

def run(url, errput: $stderr, input: $stdin, autoOpen: true)
    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.

      (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