create_session_browser.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #!/usr/bin/env python3
  2. """
  3. Requirements:
  4. pip install -r tools/requirements.txt
  5. Usage:
  6. python3 tools/create_session_browser.py <username> <password> [totp_seed] [--append sessions.jsonl] [--headless]
  7. Examples:
  8. # Output to terminal
  9. python3 tools/create_session_browser.py myusername mypassword TOTP_SECRET
  10. # Append to sessions.jsonl
  11. python3 tools/create_session_browser.py myusername mypassword TOTP_SECRET --append sessions.jsonl
  12. # Headless mode (may increase detection risk)
  13. python3 tools/create_session_browser.py myusername mypassword TOTP_SECRET --headless
  14. Output:
  15. {"kind": "cookie", "username": "...", "id": "...", "auth_token": "...", "ct0": "..."}
  16. """
  17. import asyncio
  18. import json
  19. import os
  20. import shutil
  21. import sys
  22. import tempfile
  23. import zendriver as zd
  24. from zendriver import cdp
  25. import pyotp
  26. # Disable password manager to prevent the "Save password?" bubble from
  27. # stealing focus during automated login.
  28. _SEED_PREFS = {
  29. "credentials_enable_service": False,
  30. "profile": {"password_manager_enabled": False},
  31. }
  32. _BROWSER_ARGS = [
  33. "--password-store=basic",
  34. "--no-first-run",
  35. "--no-default-browser-check",
  36. "--disable-notifications",
  37. ]
  38. def _log(*a):
  39. print(*a, file=sys.stderr, flush=True)
  40. def _make_profile():
  41. """Create a temp Chrome profile with password manager disabled."""
  42. profile = tempfile.mkdtemp(prefix="xsess_")
  43. default = os.path.join(profile, "Default")
  44. os.makedirs(default)
  45. with open(os.path.join(default, "Preferences"), "w") as f:
  46. json.dump(_SEED_PREFS, f)
  47. return profile
  48. def _extract_user_id(cookies_dict):
  49. """Extract numeric user ID from the twid cookie."""
  50. twid = cookies_dict.get("twid", "").strip('"')
  51. for prefix in ("u%3D", "u="):
  52. if prefix in twid:
  53. return twid.split(prefix)[1].split("&")[0].strip('"')
  54. return None
  55. async def _check_login_error(tab):
  56. """Check if the login flow is showing an error (wrong password, etc.)."""
  57. try:
  58. return await tab.evaluate('''(() => {
  59. // Check role="alert" elements (X's standard error display)
  60. const alert = document.querySelector('[role="alert"]');
  61. if (alert) {
  62. const t = alert.textContent.trim();
  63. if (t.length > 0 && t.length < 200) return t;
  64. }
  65. // Check for common error strings in visible text
  66. for (const el of document.querySelectorAll('p, span, div')) {
  67. const t = el.textContent.trim();
  68. if (t.length > 5 && t.length < 150
  69. && (t.includes('Wrong password')
  70. || t.includes('incorrect')
  71. || t.includes('Could not log you in')
  72. || t.includes("can\\'t find")
  73. || t.includes('cannot find')
  74. || t.includes('suspended')
  75. || t.includes('locked')
  76. || t.includes('unusual login'))) {
  77. return t;
  78. }
  79. }
  80. return '';
  81. })()''')
  82. except Exception:
  83. return ''
  84. async def _click_continue(tab):
  85. """Click the 'Continue' / 'Log in' button in the jf onboarding flow.
  86. The button is a nested <div> containing <p>Continue</p> (or <p>Log in</p>),
  87. not a standard <button type="submit">.
  88. """
  89. try:
  90. return await tab.evaluate('''(() => {
  91. for (const p of document.querySelectorAll('p.jf-element')) {
  92. const t = p.textContent.trim();
  93. if (t === 'Continue' || t === 'Log in' || t === 'Next') {
  94. p.parentElement.parentElement.parentElement.click();
  95. return true;
  96. }
  97. }
  98. return false;
  99. })()''')
  100. except Exception:
  101. return False
  102. async def _find_visible_input(tab, name, timeout=15):
  103. """Wait for a visible input[name=...] to appear and return it."""
  104. for _ in range(timeout * 2):
  105. try:
  106. found = await tab.evaluate(f'''(() => {{
  107. for (const inp of document.querySelectorAll('input[name="{name}"]')) {{
  108. const r = inp.getBoundingClientRect();
  109. if (r.width > 0 && r.height > 0) return true;
  110. }}
  111. return false;
  112. }})()''')
  113. if found:
  114. return await tab.select(f'input[name="{name}"]')
  115. except Exception:
  116. pass
  117. await asyncio.sleep(0.5)
  118. return None
  119. async def _clear_otp(tab):
  120. """Clear the 6-box OTP field so a fresh code can be entered.
  121. Selects all content in the focused input and deletes it, then re-focuses
  122. the first OTP box.
  123. """
  124. try:
  125. await tab.evaluate('''(() => {
  126. const inputs = document.querySelectorAll('input[autocomplete="one-time-code"]');
  127. if (inputs.length) {
  128. inputs.forEach(inp => { inp.value = ''; });
  129. inputs[0].focus();
  130. return true;
  131. }
  132. // Fallback: clear any focused input
  133. const el = document.activeElement;
  134. if (el && el.tagName === 'INPUT') {
  135. el.value = '';
  136. el.dispatchEvent(new Event('input', { bubbles: true }));
  137. }
  138. return false;
  139. })()''')
  140. except Exception:
  141. pass
  142. async def _type_otp(tab, code):
  143. """Type a 2FA code via CDP Input.insertText into the auto-focused OTP field.
  144. The jf onboarding 2FA screen shows 6 individual boxes that auto-focus the
  145. first one. insertText commits all digits at once; the field auto-submits
  146. when all 6 are filled. This avoids DOM/Runtime methods that can hang on
  147. this SPA screen.
  148. """
  149. try:
  150. await asyncio.wait_for(
  151. tab.send(cdp.input_.insert_text(code)), timeout=8
  152. )
  153. return True
  154. except Exception:
  155. return False
  156. async def _otp_error(tab):
  157. """Check if the 2FA screen shows an error message like 'Incorrect'."""
  158. try:
  159. return await tab.evaluate('''(() => {
  160. const el = document.querySelector('[role="alert"]');
  161. if (el) return el.textContent.trim().substring(0, 80);
  162. for (const el of document.querySelectorAll('p, span')) {
  163. const t = el.textContent.trim();
  164. if (t.length < 100
  165. && (t.includes('Incorrect') || t.includes('try again')
  166. || t.includes('invalid') || t.includes('expired'))) {
  167. return t;
  168. }
  169. }
  170. return '';
  171. })()''')
  172. except Exception:
  173. return ''
  174. def _fresh_totp(totp_seed, min_remaining=5):
  175. """Generate a TOTP code with at least min_remaining seconds of validity.
  176. If the current code is about to expire, waits for the next window.
  177. """
  178. import time
  179. totp = pyotp.TOTP(totp_seed)
  180. code = totp.now()
  181. # Check remaining validity: TOTP period is 30s
  182. elapsed = time.time() % 30
  183. remaining = 30 - elapsed
  184. if remaining < min_remaining:
  185. time.sleep(remaining + 1)
  186. code = totp.now()
  187. return code
  188. async def _get_cookies(browser):
  189. """Read cookies from the browser, returning a name→value dict."""
  190. cookies = await browser.cookies.get_all()
  191. return {c.name: c.value for c in cookies}
  192. async def _check_session(browser, username):
  193. """Check if auth cookies are present and build a session dict."""
  194. cd = await _get_cookies(browser)
  195. if "auth_token" in cd and "ct0" in cd:
  196. return {
  197. "kind": "cookie",
  198. "username": username,
  199. "id": _extract_user_id(cd),
  200. "auth_token": cd["auth_token"],
  201. "ct0": cd["ct0"],
  202. }
  203. return None
  204. async def login_and_get_session(username, password, totp_seed=None, headless=False):
  205. """Authenticate with X.com and return a session dict, or None on failure.
  206. Uses the new /i/jf/onboarding flow (as of mid-2026). A fresh Chrome
  207. profile is created per login to avoid cookie bleed.
  208. """
  209. profile = _make_profile()
  210. browser = await zd.start(
  211. headless=headless,
  212. user_data_dir=profile,
  213. browser_args=_BROWSER_ARGS,
  214. )
  215. try:
  216. # --- Navigate to login ---
  217. _log(f"[*] Logging in {username}...")
  218. tab = await browser.get("https://x.com/i/flow/login")
  219. await asyncio.sleep(4)
  220. # --- Username ---
  221. _log("[*] Entering username...")
  222. uinput = await _find_visible_input(tab, "username_or_email")
  223. if not uinput:
  224. raise Exception("Username field not found")
  225. await uinput.click()
  226. await asyncio.sleep(0.3)
  227. await uinput.send_keys(username)
  228. await asyncio.sleep(0.5)
  229. if not await _click_continue(tab):
  230. await uinput.send_keys("\n")
  231. await asyncio.sleep(3)
  232. err = await _check_login_error(tab)
  233. if err:
  234. raise Exception(f"Username rejected: {err}")
  235. # --- Password ---
  236. _log("[*] Entering password...")
  237. pw = await _find_visible_input(tab, "password")
  238. if not pw:
  239. raise Exception("Password field not found")
  240. await pw.click()
  241. await asyncio.sleep(0.3)
  242. await pw.send_keys(password)
  243. await asyncio.sleep(0.5)
  244. if not await _click_continue(tab):
  245. await pw.send_keys("\n")
  246. await asyncio.sleep(3)
  247. err = await _check_login_error(tab)
  248. if err:
  249. raise Exception(f"Login failed: {err}")
  250. # --- Check for immediate auth (no 2FA) ---
  251. session = await _check_session(browser, username)
  252. if session:
  253. _log("[*] Authenticated (no 2FA)")
  254. return session
  255. # --- 2FA ---
  256. # Detect 2FA by URL fragment (reliable) or page content
  257. for _ in range(10):
  258. url = tab.url or ""
  259. if "two_factor" in url:
  260. break
  261. await asyncio.sleep(1)
  262. await asyncio.sleep(1) # let the OTP field mount and auto-focus
  263. url = tab.url or ""
  264. if "two_factor" in url:
  265. if not totp_seed:
  266. raise Exception("2FA required but no TOTP seed provided")
  267. _log("[*] 2FA detected, entering code...")
  268. last_code = None
  269. for attempt in range(2):
  270. code = _fresh_totp(totp_seed)
  271. while code == last_code:
  272. await asyncio.sleep(3)
  273. code = _fresh_totp(totp_seed)
  274. last_code = code
  275. if attempt > 0:
  276. await _clear_otp(tab)
  277. await asyncio.sleep(0.5)
  278. typed = await _type_otp(tab, code)
  279. _log(f"[*] OTP attempt {attempt + 1}: typed={typed}")
  280. # Check for success or error (fast loop)
  281. for _ in range(5):
  282. await asyncio.sleep(2)
  283. session = await _check_session(browser, username)
  284. if session:
  285. _log("[*] Authenticated (2FA)")
  286. return session
  287. err = await _otp_error(tab)
  288. if err:
  289. _log(f"[*] OTP rejected: {err}")
  290. break
  291. raise Exception("2FA code rejected (account may be suspended or OTP reset)")
  292. # --- Post-login interstitials (premium signup push, etc.) ---
  293. _log("[*] Waiting for post-login redirect...")
  294. for i in range(10):
  295. session = await _check_session(browser, username)
  296. if session:
  297. _log("[*] Authenticated")
  298. return session
  299. await _click_continue(tab)
  300. await asyncio.sleep(2)
  301. raise Exception("Timeout waiting for authentication cookies")
  302. finally:
  303. try:
  304. await browser.stop()
  305. except Exception:
  306. pass
  307. await asyncio.sleep(1)
  308. shutil.rmtree(profile, ignore_errors=True)
  309. async def main():
  310. if len(sys.argv) < 3:
  311. print(
  312. "Usage: python3 create_session_browser.py username password"
  313. " [totp_seed] [--append file.jsonl] [--headless]"
  314. )
  315. sys.exit(1)
  316. username = sys.argv[1]
  317. password = sys.argv[2]
  318. totp_seed = None
  319. append_file = None
  320. headless = False
  321. # Parse optional arguments
  322. i = 3
  323. while i < len(sys.argv):
  324. arg = sys.argv[i]
  325. if arg == "--append":
  326. if i + 1 < len(sys.argv):
  327. append_file = sys.argv[i + 1]
  328. i += 2
  329. else:
  330. print("[!] Error: --append requires a filename", file=sys.stderr)
  331. sys.exit(1)
  332. elif arg == "--headless":
  333. headless = True
  334. i += 1
  335. elif not arg.startswith("--"):
  336. if totp_seed is None:
  337. totp_seed = arg
  338. i += 1
  339. else:
  340. print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr)
  341. i += 1
  342. try:
  343. session = await login_and_get_session(username, password, totp_seed, headless)
  344. output = json.dumps(session)
  345. if append_file:
  346. with open(append_file, "a") as f:
  347. f.write(output + "\n")
  348. print(f"✓ Session appended to {append_file}", file=sys.stderr)
  349. else:
  350. print(output)
  351. os._exit(0)
  352. except Exception as error:
  353. print(f"[!] Error: {error}", file=sys.stderr)
  354. sys.exit(1)
  355. if __name__ == "__main__":
  356. asyncio.run(main())