Module: ChromeAuth
- Defined in:
- lib/ChromeAuth.rb
Overview
Drive a visible Chrome (via ferrum / CDP) to let the user sign into Medium in a real browser, then read sid/uid/cf_clearance/_cfuvid back out of the session. Used both for first-time setup (no cookies on disk) and as the Cloudflare-block recovery flow (cf_clearance refresh).
“Headless” in the user’s spec is a misnomer — login is interactive, so we launch with headless:false and rely on the user to complete the login in the visible window before pressing Enter.
Constant Summary collapse
- TARGET_COOKIES =
%w[sid uid cf_clearance _cfuvid].freeze
- LOGIN_URL =
'https://medium.com/m/signin'.freeze
- REFRESH_URL =
'https://medium.com'.freeze
- @@session =
nil
Class Method Summary collapse
-
.available? ⇒ Boolean
True iff ferrum loads AND a Chrome binary is detectable.
-
.buildBrowser ⇒ Object
Factory split out so tests can stub it.
- .cancelSession! ⇒ Object
-
.collectMediumCookies(browser) ⇒ Object
Filter the browser’s cookie jar down to medium.com cookies whose name is one of TARGET_COOKIES.
- .finishSession! ⇒ Object
-
.login!(errput: $stderr, input: $stdin, openURL: LOGIN_URL) ⇒ Object
—- Single-shot CLI flow ————————————— Open Chrome at openURL, wait for the user to press Enter, then collect the four target cookies.
- .mediumDomain?(domain) ⇒ Boolean
- .promptUser(errput, input, url) ⇒ Object
- .sessionActive? ⇒ Boolean
-
.startSession!(openURL: LOGIN_URL) ⇒ Object
—- Split flow for MCP / other long-lived hosts —————- ‘startSession!` / `finishSession!` / `cancelSession!` let a caller spawn the browser in one tool call and harvest cookies in another, using the host process (e.g. an MCP server) as the place that holds the still-open browser between calls.
Class Method Details
.available? ⇒ Boolean
True iff ferrum loads AND a Chrome binary is detectable. Anything else returns false so the caller can fall back to the legacy default-browser flow without aborting.
23 24 25 26 27 28 29 |
# File 'lib/ChromeAuth.rb', line 23 def available? require 'ferrum' path = Ferrum::Browser::Options::Chrome..detect_path !path.nil? && !path.empty? rescue LoadError, StandardError false end |
.buildBrowser ⇒ Object
Factory split out so tests can stub it. Tweaking ferrum options globally (window size, timeouts) belongs here too.
110 111 112 113 114 115 116 117 118 |
# File 'lib/ChromeAuth.rb', line 110 def buildBrowser require 'ferrum' Ferrum::Browser.new( headless: false, window_size: [1280, 900], timeout: 60, process_timeout: 30 ) end |
.cancelSession! ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/ChromeAuth.rb', line 92 def cancelSession! return false unless sessionActive? browser = @@session[:browser] @@session = nil begin browser&.quit rescue StandardError # ignore: best-effort shutdown end true end |
.collectMediumCookies(browser) ⇒ Object
Filter the browser’s cookie jar down to medium.com cookies whose name is one of TARGET_COOKIES. We accept both .medium.com and medium.com because Cloudflare sets _cfuvid on the apex while Medium tends to set sid/uid on the dot-prefixed domain.
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/ChromeAuth.rb', line 124 def collectMediumCookies(browser) result = {} browser..each do || next unless TARGET_COOKIES.include?(.name) next unless mediumDomain?(.domain) result[.name] = .value end result rescue StandardError {} end |
.finishSession! ⇒ Object
82 83 84 85 86 87 88 89 90 |
# File 'lib/ChromeAuth.rb', line 82 def finishSession! raise 'No active ChromeAuth session — call startSession! first.' unless sessionActive? browser = @@session[:browser] = collectMediumCookies(browser) CookieCache.save(CookieCache.load.merge()) if .any? ensure cancelSession! end |
.login!(errput: $stderr, input: $stdin, openURL: LOGIN_URL) ⇒ Object
—- Single-shot CLI flow ————————————— Open Chrome at openURL, wait for the user to press Enter, then collect the four target cookies. Returns hash { ‘sid’ => ‘…’, … } — keys missing from the browser are simply omitted, so callers must check what came back rather than assume completeness.
Raises StandardError on browser launch / navigation failure; callers are expected to rescue and degrade gracefully.
39 40 41 42 43 44 45 46 |
# File 'lib/ChromeAuth.rb', line 39 def login!(errput: $stderr, input: $stdin, openURL: LOGIN_URL) startSession!(openURL: openURL) promptUser(errput, input, openURL) finishSession! rescue StandardError cancelSession! raise end |
.mediumDomain?(domain) ⇒ Boolean
136 137 138 139 140 |
# File 'lib/ChromeAuth.rb', line 136 def mediumDomain?(domain) return false if domain.nil? normalized = domain.start_with?('.') ? domain[1..] : domain normalized == 'medium.com' || normalized.end_with?('.medium.com') end |
.promptUser(errput, input, url) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/ChromeAuth.rb', line 142 def promptUser(errput, input, url) errput.puts <<~MSG ────────────────────────────────────────────────────────────────────── 🔐 Sign into Medium in the Chrome window that just opened. Steps: 1. Complete login (and any Cloudflare challenge) at #{url}. 2. Stay on a medium.com page once you're signed in. 3. Come back here and press Enter — we'll read sid / uid / cf_clearance / _cfuvid out of the browser and cache them at #{CookieCache.path}. (Press Ctrl-D to abort and fall back to manual setup.) ────────────────────────────────────────────────────────────────────── MSG errput.print 'Press Enter when signed in… ' line = input.gets errput.puts line end |
.sessionActive? ⇒ Boolean
104 105 106 |
# File 'lib/ChromeAuth.rb', line 104 def sessionActive? !@@session.nil? end |
.startSession!(openURL: LOGIN_URL) ⇒ Object
—- Split flow for MCP / other long-lived hosts —————- ‘startSession!` / `finishSession!` / `cancelSession!` let a caller spawn the browser in one tool call and harvest cookies in another, using the host process (e.g. an MCP server) as the place that holds the still-open browser between calls.
Lifecycle:
startSession! → opens browser, returns immediately. If a session
is already alive, that one is force-cancelled
first so a stale browser can't strand cookies.
finishSession! → reads cookies from the live browser, writes
cache, quits browser, clears session, returns
the cookies hash.
cancelSession! → quit + clear; idempotent.
Not thread-safe: assumes a single MCP request handler at a time.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/ChromeAuth.rb', line 64 def startSession!(openURL: LOGIN_URL) cancelSession! if sessionActive? browser = buildBrowser browser.go_to(openURL) @@session = { browser: browser, openURL: openURL, startedAt: Time.now } { ok: true, openURL: openURL } rescue StandardError # If go_to or anything else blew up, make sure we don't leave a # half-built browser around with no handle. begin browser&.quit rescue StandardError # ignore end @@session = nil raise end |