"""Open a local browser and return this session's custom-scheme callback. Run on your computer (Python 3.11+): python3 -m pip install playwright python3 browser_helper.py --server https://safarivc.huitex.top Requires an installed Chrome or Edge browser. Credentials are prompted, never accepted on the command line or printed. """ import argparse import asyncio import getpass import json import os from pathlib import Path import shutil import subprocess import tempfile import time import urllib.parse import urllib.request from playwright.async_api import async_playwright def request(base, sid, token, callback=None): path = '/api/sessions/' + sid + ('/callback' if callback else '') req = urllib.request.Request(base + path, data=json.dumps({'url': callback}).encode() if callback else None, headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}) # Never forward session credentials through redirects. class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None with urllib.request.build_opener(NoRedirect).open(req, timeout=90) as response: return json.load(response) async def run(base, sid, token, browser_path): deadline = time.monotonic() + 900 while time.monotonic() < deadline: row = await asyncio.to_thread(request, base, sid, token) if row['status'] == 'awaiting_user': break if row['status'] in {'succeeded', 'failed', 'expired', 'cancelled', 'exchanging'}: print('Session is not awaiting login:', row['status']) return await asyncio.sleep(2) else: raise RuntimeError('Session timed out') login = urllib.parse.urlsplit(row['login_url']) if login.scheme != 'https' or login.hostname != 'accounts.google.com': raise ValueError('Unexpected login origin') redirect = urllib.parse.urlsplit(urllib.parse.parse_qs(login.query)['redirect_uri'][0]) callback = asyncio.get_running_loop().create_future() def observe(url): candidate = urllib.parse.urlsplit(url) if ((candidate.scheme, candidate.netloc, candidate.path) == (redirect.scheme, redirect.netloc, redirect.path) and not callback.done()): callback.set_result(url) with tempfile.TemporaryDirectory(prefix='safarivc-browser-') as profile: process = subprocess.Popen([browser_path, '--user-data-dir='+profile, '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: port_file = Path(profile) / 'DevToolsActivePort' for _ in range(100): if port_file.exists(): break if process.poll() is not None: raise RuntimeError('Browser exited') await asyncio.sleep(.1) port = int(port_file.read_text().splitlines()[0]) async with async_playwright() as pw: browser = await pw.chromium.connect_over_cdp(f'http://127.0.0.1:{port}') context = browser.contexts[0] page = await context.new_page() context.on('request', lambda req: observe(req.url)) async def response(res): try: location = await res.header_value('location') if location: observe(urllib.parse.urljoin(res.url, location)) except Exception: pass context.on('response', response) cdp = await context.new_cdp_session(page) await cdp.send('Page.enable') await cdp.send('Network.enable') cdp.on('Page.frameRequestedNavigation', lambda value: observe(value['url'])) cdp.on('Network.requestWillBeSent', lambda value: observe(value['request']['url'])) print('Complete login in the browser. Passwords are not read by this helper.') await page.goto(row['login_url'], wait_until='domcontentloaded') url = await asyncio.wait_for(callback, max(1, row['expires_at'] - time.time())) result = await asyncio.to_thread(request, base, sid, token, url) print('Session status:', result['status'], '— view the result on the service page.') await browser.close() finally: if process.poll() is None: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait() def find_browser(explicit): candidates = [explicit] if explicit else [ shutil.which('google-chrome'), shutil.which('chromium'), shutil.which('microsoft-edge'), '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', str(Path(os.environ.get('PROGRAMFILES', '')) / 'Google/Chrome/Application/chrome.exe'), str(Path(os.environ.get('PROGRAMFILES(X86)', '')) / 'Microsoft/Edge/Application/msedge.exe'), str(Path(os.environ.get('LOCALAPPDATA', '')) / 'Google/Chrome/Application/chrome.exe')] for candidate in candidates: if candidate and Path(candidate).is_file(): return candidate raise RuntimeError('Install Chrome or Edge, or pass --browser /path/to/browser') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--server', required=True, help='HTTPS service origin; localhost HTTP is allowed') parser.add_argument('--browser', help='Optional Chrome or Edge executable path') args = parser.parse_args() base = args.server.rstrip('/') parts = urllib.parse.urlsplit(base) if (parts.username or parts.password or parts.query or parts.fragment or parts.path or not parts.hostname or (parts.scheme != 'https' and not ( parts.scheme == 'http' and parts.hostname in {'127.0.0.1', 'localhost', '::1'}))): parser.error('Use an HTTPS origin, or HTTP on localhost') sid = input('Session ID: ').strip() if len(sid) != 32 or any(c not in '0123456789abcdef' for c in sid): parser.error('Invalid session ID') token = getpass.getpass('Session credential: ').strip() try: asyncio.run(run(base, sid, token, find_browser(args.browser))) except (Exception, KeyboardInterrupt) as exc: print('Helper stopped:', type(exc).__name__, '— check the session page; no automatic exchange retry.') raise SystemExit(1) from None if __name__ == '__main__': main()