#!/usr/bin/env python3 """ Login helper using undetected_chromedriver to bypass Cloudflare Turnstile. Runs Chrome in headed mode with Xvfb (virtual display) so Turnstile sees a real browser. Usage: xvfb-run python3 login_helper.py Outputs JSON to stdout: {"ok": true, "cookies": "name=val; name2=val2", "url": ""} {"ok": false, "error": "reason"} """ import sys import json import time import os import shutil def main(): if len(sys.argv) < 4: print(json.dumps({"ok": False, "error": "Usage: login_helper.py "})) sys.exit(1) login_url = sys.argv[1] username = sys.argv[2] password = sys.argv[3] try: import undetected_chromedriver as uc from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC except ImportError as e: print(json.dumps({"ok": False, "error": f"Missing dependency: {e}"})) sys.exit(1) driver = None try: # Find chromium binary chromium_path = os.environ.get('CHROMIUM_PATH', '') if not chromium_path or not os.path.exists(chromium_path): for p in ['/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/lib/chromium/chromium']: if os.path.exists(p): chromium_path = p break # Find system chromedriver (Alpine: chromium-chromedriver package) chromedriver_path = None for p in ['/usr/bin/chromedriver', '/usr/lib/chromium/chromedriver']: if os.path.exists(p): chromedriver_path = p break # Get chromium version for undetected_chromedriver version_main = None try: import subprocess result = subprocess.run([chromium_path, '--version'], capture_output=True, text=True, timeout=5) # e.g. "Chromium 131.0.6778.139" or "Chromium 131.0.6778.139 Alpine Linux" parts = result.stdout.strip().split() ver_str = None for part in parts: if '.' in part and part[0].isdigit(): ver_str = part break if ver_str: version_main = int(ver_str.split('.')[0]) log(f"Chromium version: {ver_str} (major: {version_main})") except Exception as e: log(f"Could not detect chromium version: {e}") options = uc.ChromeOptions() options.binary_location = chromium_path options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-gpu') options.add_argument('--window-size=1920,1080') log(f"Chromium: {chromium_path}") log(f"Chromedriver: {chromedriver_path}") # Create the driver # Use system chromedriver to avoid downloading (fails on Alpine/musl) driver = uc.Chrome( options=options, driver_executable_path=chromedriver_path, headless=False, version_main=version_main, ) driver.set_window_size(1920, 1080) log(f"Navigating to {login_url}...") driver.get(login_url) # Wait for DDoS-Guard to solve and login form to appear log("Waiting for login form (DDoS-Guard solving)...") WebDriverWait(driver, 60).until( EC.presence_of_element_located((By.CSS_SELECTOR, 'input[name="login"]')) ) log("Login form found") # Wait for Turnstile to auto-solve (should work in undetected headed mode) log("Waiting for Turnstile to solve...") turnstile_token = "" for i in range(45): try: el = driver.find_element(By.CSS_SELECTOR, 'input[name="cf-turnstile-response"]') val = el.get_attribute("value") if val: turnstile_token = val break except Exception: pass time.sleep(1) if i % 10 == 9: log(f"Still waiting for Turnstile... ({i+1}s)") if turnstile_token: log(f"Turnstile solved (token: {turnstile_token[:20]}...)") else: log("Warning: Turnstile token not found after 45s — attempting login anyway") # Fill the login form log("Filling login form...") login_input = driver.find_element(By.CSS_SELECTOR, 'input[name="login"]') login_input.clear() # Type slowly to appear human for ch in username: login_input.send_keys(ch) time.sleep(0.03) pass_input = driver.find_element(By.CSS_SELECTOR, 'input[name="password"]') pass_input.clear() for ch in password: pass_input.send_keys(ch) time.sleep(0.03) # Check remember checkbox try: remember = driver.find_element(By.CSS_SELECTOR, 'input[name="remember"]') if not remember.is_selected(): driver.execute_script("arguments[0].checked = true;", remember) except Exception: pass # Submit form log("Submitting login form...") try: submit_btn = driver.find_element(By.CSS_SELECTOR, 'button[type="submit"], input[type="submit"], .button--primary') submit_btn.click() except Exception: driver.execute_script(""" var form = document.querySelector('form.block-body') || document.querySelector('form[action*="login"]'); if (form) form.submit(); """) # Wait for navigation after submit log("Waiting for redirect...") time.sleep(5) try: WebDriverWait(driver, 15).until( lambda d: d.execute_script("return document.readyState") == "complete" ) except Exception: pass final_url = driver.current_url log(f"After submit: {final_url}") # Extract cookies cookies = driver.get_cookies() cookie_str = "; ".join(f"{c['name']}={c['value']}" for c in cookies) # Check for login success has_user_cookie = any(c['name'] in ('xf_user', 'ogaddgmetaprof_user') for c in cookies) if not has_user_cookie: # Check for error message error_msg = "Login failed — no user cookie returned" try: error_el = driver.find_element(By.CSS_SELECTOR, '.blockMessage--error') error_msg = error_el.text.strip() except Exception: pass # Also dump all cookie names for debugging cookie_names = [c['name'] for c in cookies] log(f"Cookie names: {cookie_names}") log(f"Error: {error_msg}") print(json.dumps({"ok": False, "error": error_msg, "url": final_url})) sys.exit(1) log(f"Login successful — {len(cookies)} cookies") print(json.dumps({"ok": True, "cookies": cookie_str, "url": final_url})) except Exception as e: log(f"Fatal error: {e}") print(json.dumps({"ok": False, "error": str(e)})) sys.exit(1) finally: if driver: try: driver.quit() except Exception: pass def log(msg): """Log to stderr so it doesn't interfere with JSON stdout.""" print(f"[login_helper] {msg}", file=sys.stderr, flush=True) if __name__ == "__main__": main()