Rewrite session scripts for new login flow

Fixes #1421
This commit is contained in:
Zed 2026-08-17 05:28:53 +07:00
commit 607765c463
4 changed files with 375 additions and 235 deletions

View file

@ -23,118 +23,353 @@ Output:
import asyncio
import json
import os
import shutil
import sys
import tempfile
import nodriver as uc
import zendriver as zd
from zendriver import cdp
import pyotp
async def login_and_get_cookies(username, password, totp_seed=None, headless=False):
"""Authenticate with X.com and extract session cookies"""
# Note: headless mode may increase detection risk from bot-detection systems
browser = await uc.start(headless=headless)
tab = await browser.get("https://x.com/i/flow/login")
# Disable password manager to prevent the "Save password?" bubble from
# stealing focus during automated login.
_SEED_PREFS = {
"credentials_enable_service": False,
"profile": {"password_manager_enabled": False},
}
_BROWSER_ARGS = [
"--password-store=basic",
"--no-first-run",
"--no-default-browser-check",
"--disable-notifications",
]
def _log(*a):
print(*a, file=sys.stderr, flush=True)
def _make_profile():
"""Create a temp Chrome profile with password manager disabled."""
profile = tempfile.mkdtemp(prefix="xsess_")
default = os.path.join(profile, "Default")
os.makedirs(default)
with open(os.path.join(default, "Preferences"), "w") as f:
json.dump(_SEED_PREFS, f)
return profile
def _extract_user_id(cookies_dict):
"""Extract numeric user ID from the twid cookie."""
twid = cookies_dict.get("twid", "").strip('"')
for prefix in ("u%3D", "u="):
if prefix in twid:
return twid.split(prefix)[1].split("&")[0].strip('"')
return None
async def _check_login_error(tab):
"""Check if the login flow is showing an error (wrong password, etc.)."""
try:
# Enter username
print(f"[*] Entering username {username}...", file=sys.stderr)
return await tab.evaluate('''(() => {
// Check role="alert" elements (X's standard error display)
const alert = document.querySelector('[role="alert"]');
if (alert) {
const t = alert.textContent.trim();
if (t.length > 0 && t.length < 200) return t;
}
// Check for common error strings in visible text
for (const el of document.querySelectorAll('p, span, div')) {
const t = el.textContent.trim();
if (t.length > 5 && t.length < 150
&& (t.includes('Wrong password')
|| t.includes('incorrect')
|| t.includes('Could not log you in')
|| t.includes("can\\'t find")
|| t.includes('cannot find')
|| t.includes('suspended')
|| t.includes('locked')
|| t.includes('unusual login'))) {
return t;
}
}
return '';
})()''')
except Exception:
return ''
retry = 0
while retry < 5:
username_input = await tab.find(
'input[autocomplete="username"]', timeout=10
)
pos = await username_input.get_position()
await tab.mouse_move(pos.x, pos.y, steps=50, flash=True)
await asyncio.sleep(0.1)
async def _click_continue(tab):
"""Click the 'Continue' / 'Log in' button in the jf onboarding flow.
await username_input.click()
The button is a nested <div> containing <p>Continue</p> (or <p>Log in</p>),
not a standard <button type="submit">.
"""
try:
return await tab.evaluate('''(() => {
for (const p of document.querySelectorAll('p.jf-element')) {
const t = p.textContent.trim();
if (t === 'Continue' || t === 'Log in' || t === 'Next') {
p.parentElement.parentElement.parentElement.click();
return true;
}
}
return false;
})()''')
except Exception:
return False
async def _find_visible_input(tab, name, timeout=15):
"""Wait for a visible input[name=...] to appear and return it."""
for _ in range(timeout * 2):
try:
found = await tab.evaluate(f'''(() => {{
for (const inp of document.querySelectorAll('input[name="{name}"]')) {{
const r = inp.getBoundingClientRect();
if (r.width > 0 && r.height > 0) return true;
}}
return false;
}})()''')
if found:
return await tab.select(f'input[name="{name}"]')
except Exception:
pass
await asyncio.sleep(0.5)
await username_input.send_keys(username)
await asyncio.sleep(0.2)
await username_input.send_keys("\n")
await asyncio.sleep(2)
return None
page_content = await tab.get_content()
if "Could not log you in" in page_content:
retry += 1
wait = retry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Enter password
print("[*] Entering password...", file=sys.stderr)
pretry = 0
while pretry < 5:
password_input = await tab.find(
'input[autocomplete="current-password"]', timeout=15
async def _clear_otp(tab):
"""Clear the 6-box OTP field so a fresh code can be entered.
Selects all content in the focused input and deletes it, then re-focuses
the first OTP box.
"""
try:
await tab.evaluate('''(() => {
const inputs = document.querySelectorAll('input[autocomplete="one-time-code"]');
if (inputs.length) {
inputs.forEach(inp => { inp.value = ''; });
inputs[0].focus();
return true;
}
// Fallback: clear any focused input
const el = document.activeElement;
if (el && el.tagName === 'INPUT') {
el.value = '';
el.dispatchEvent(new Event('input', { bubbles: true }));
}
return false;
})()''')
except Exception:
pass
async def _type_otp(tab, code):
"""Type a 2FA code via CDP Input.insertText into the auto-focused OTP field.
The jf onboarding 2FA screen shows 6 individual boxes that auto-focus the
first one. insertText commits all digits at once; the field auto-submits
when all 6 are filled. This avoids DOM/Runtime methods that can hang on
this SPA screen.
"""
try:
await asyncio.wait_for(
tab.send(cdp.input_.insert_text(code)), timeout=8
)
await password_input.click()
return True
except Exception:
return False
async def _otp_error(tab):
"""Check if the 2FA screen shows an error message like 'Incorrect'."""
try:
return await tab.evaluate('''(() => {
const el = document.querySelector('[role="alert"]');
if (el) return el.textContent.trim().substring(0, 80);
for (const el of document.querySelectorAll('p, span')) {
const t = el.textContent.trim();
if (t.length < 100
&& (t.includes('Incorrect') || t.includes('try again')
|| t.includes('invalid') || t.includes('expired'))) {
return t;
}
}
return '';
})()''')
except Exception:
return ''
def _fresh_totp(totp_seed, min_remaining=5):
"""Generate a TOTP code with at least min_remaining seconds of validity.
If the current code is about to expire, waits for the next window.
"""
import time
totp = pyotp.TOTP(totp_seed)
code = totp.now()
# Check remaining validity: TOTP period is 30s
elapsed = time.time() % 30
remaining = 30 - elapsed
if remaining < min_remaining:
time.sleep(remaining + 1)
code = totp.now()
return code
async def _get_cookies(browser):
"""Read cookies from the browser, returning a name→value dict."""
cookies = await browser.cookies.get_all()
return {c.name: c.value for c in cookies}
async def _check_session(browser, username):
"""Check if auth cookies are present and build a session dict."""
cd = await _get_cookies(browser)
if "auth_token" in cd and "ct0" in cd:
return {
"kind": "cookie",
"username": username,
"id": _extract_user_id(cd),
"auth_token": cd["auth_token"],
"ct0": cd["ct0"],
}
return None
async def login_and_get_session(username, password, totp_seed=None, headless=False):
"""Authenticate with X.com and return a session dict, or None on failure.
Uses the new /i/jf/onboarding flow (as of mid-2026). A fresh Chrome
profile is created per login to avoid cookie bleed.
"""
profile = _make_profile()
browser = await zd.start(
headless=headless,
user_data_dir=profile,
browser_args=_BROWSER_ARGS,
)
try:
# --- Navigate to login ---
_log(f"[*] Logging in {username}...")
tab = await browser.get("https://x.com/i/flow/login")
await asyncio.sleep(4)
# --- Username ---
_log("[*] Entering username...")
uinput = await _find_visible_input(tab, "username_or_email")
if not uinput:
raise Exception("Username field not found")
await uinput.click()
await asyncio.sleep(0.3)
await uinput.send_keys(username)
await asyncio.sleep(0.5)
await password_input.send_keys(password)
await asyncio.sleep(0.2)
await password_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
pretry += 1
wait = pretry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
if not await _click_continue(tab):
await uinput.send_keys("\n")
await asyncio.sleep(3)
err = await _check_login_error(tab)
if err:
raise Exception(f"Username rejected: {err}")
# --- Password ---
_log("[*] Entering password...")
pw = await _find_visible_input(tab, "password")
if not pw:
raise Exception("Password field not found")
await pw.click()
await asyncio.sleep(0.3)
await pw.send_keys(password)
await asyncio.sleep(0.5)
if not await _click_continue(tab):
await pw.send_keys("\n")
await asyncio.sleep(3)
err = await _check_login_error(tab)
if err:
raise Exception(f"Login failed: {err}")
# --- Check for immediate auth (no 2FA) ---
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated (no 2FA)")
return session
# --- 2FA ---
# Detect 2FA by URL fragment (reliable) or page content
for _ in range(10):
url = tab.url or ""
if "two_factor" in url:
break
await asyncio.sleep(1)
await asyncio.sleep(1) # let the OTP field mount and auto-focus
# Handle 2FA if needed
page_content = await tab.get_content()
if "verification code" in page_content or "Enter code" in page_content:
url = tab.url or ""
if "two_factor" in url:
if not totp_seed:
raise Exception("2FA required but no TOTP seed provided")
print("[*] 2FA detected, entering code...", file=sys.stderr)
totp_code = pyotp.TOTP(totp_seed).now()
code_input = await tab.select('input[type="text"]')
await code_input.send_keys(totp_code + "\n")
_log("[*] 2FA detected, entering code...")
last_code = None
for attempt in range(2):
code = _fresh_totp(totp_seed)
while code == last_code:
await asyncio.sleep(3)
code = _fresh_totp(totp_seed)
last_code = code
# Get cookies
print("[*] Retrieving cookies...", file=sys.stderr)
for _ in range(20): # 20 second timeout
cookies = await browser.cookies.get_all()
cookies_dict = {cookie.name: cookie.value for cookie in cookies}
if attempt > 0:
await _clear_otp(tab)
await asyncio.sleep(0.5)
if "auth_token" in cookies_dict and "ct0" in cookies_dict:
# Extract ID from twid cookie (may be URL-encoded)
user_id = None
if "twid" in cookies_dict:
twid = cookies_dict["twid"]
# Try to extract the ID from twid (format: u%3D<id> or u=<id>)
if "u%3D" in twid:
user_id = twid.split("u%3D")[1].split("&")[0].strip('"')
elif "u=" in twid:
user_id = twid.split("u=")[1].split("&")[0].strip('"')
typed = await _type_otp(tab, code)
_log(f"[*] OTP attempt {attempt + 1}: typed={typed}")
cookies_dict["username"] = username
if user_id:
cookies_dict["id"] = user_id
# Check for success or error (fast loop)
for _ in range(5):
await asyncio.sleep(2)
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated (2FA)")
return session
err = await _otp_error(tab)
if err:
_log(f"[*] OTP rejected: {err}")
break
return cookies_dict
raise Exception("2FA code rejected (account may be suspended or OTP reset)")
await asyncio.sleep(1)
# --- Post-login interstitials (premium signup push, etc.) ---
_log("[*] Waiting for post-login redirect...")
for i in range(10):
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated")
return session
await _click_continue(tab)
await asyncio.sleep(2)
raise Exception("Timeout waiting for cookies")
raise Exception("Timeout waiting for authentication cookies")
finally:
browser.stop()
try:
await browser.stop()
except Exception:
pass
await asyncio.sleep(1)
shutil.rmtree(profile, ignore_errors=True)
async def main():
if len(sys.argv) < 3:
print(
"Usage: python3 create_session_browser.py username password [totp_seed] [--append file.jsonl] [--headless]"
"Usage: python3 create_session_browser.py username password"
" [totp_seed] [--append file.jsonl] [--headless]"
)
sys.exit(1)
@ -151,7 +386,7 @@ async def main():
if arg == "--append":
if i + 1 < len(sys.argv):
append_file = sys.argv[i + 1]
i += 2 # Skip '--append' and filename
i += 2
else:
print("[!] Error: --append requires a filename", file=sys.stderr)
sys.exit(1)
@ -163,19 +398,11 @@ async def main():
totp_seed = arg
i += 1
else:
# Unkown args
print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr)
i += 1
try:
cookies = await login_and_get_cookies(username, password, totp_seed, headless)
session = {
"kind": "cookie",
"username": cookies["username"],
"id": cookies.get("id"),
"auth_token": cookies["auth_token"],
"ct0": cookies["ct0"],
}
session = await login_and_get_session(username, password, totp_seed, headless)
output = json.dumps(session)
if append_file:

View file

@ -1,5 +1,11 @@
#!/usr/bin/env python3
"""
DEPRECATED: X now requires a castle token generated by client-side JavaScript
during the login flow, which blocks pure-API authentication. Use
create_session_browser.py instead.
This script is kept for reference but will fail with current X API defenses.
Requirements:
pip install curl_cffi pyotp

View file

@ -13,14 +13,14 @@ Examples:
# Append to sessions.jsonl
python3 tools/create_sessions_browser.py <accounts_file> --append sessions.jsonl
# Add 5 second delay between sessions (default: 1)
# Add 5 second delay between sessions (default: 3)
python3 tools/create_sessions_browser.py <accounts_file> --delay 5
# Headless mode (may increase detection risk)
python3 tools/create_sessions_browser.py <accounts_file> --headless
Input (accounts_file):
[{"username": "user", "password": "pass", "totp": "totp_code"}, {...}, ...]
[{"username": "user", "password": "pass", "totp": "totp_secret"}, {...}, ...]
Output:
{"kind": "cookie", "username": "...", "id": "...", "auth_token": "...", "ct0": "..."}
@ -33,127 +33,21 @@ import json
import sys
from time import sleep
import nodriver as uc
import pyotp
async def login_and_get_cookies(account, headless=False):
"""Authenticate with X.com and extract session cookies"""
# Note: headless mode may increase detection risk from bot-detection systems
browser = await uc.start(headless=headless)
tab = await browser.get("https://x.com/i/flow/login")
username = account["username"]
password = account["password"]
totp_seed = account["totp"]
try:
# Enter username
print(f"[*] Entering username {username}...", file=sys.stderr)
retry = 0
while retry < 5:
username_input = await tab.find(
'input[autocomplete="username"]', timeout=10
)
pos = await username_input.get_position()
await tab.mouse_move(pos.x, pos.y, steps=50, flash=True)
await asyncio.sleep(0.1)
await username_input.click()
await asyncio.sleep(0.5)
await username_input.send_keys(username)
await asyncio.sleep(0.2)
await username_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
retry += 1
wait = retry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Enter password
print("[*] Entering password...", file=sys.stderr)
pretry = 0
while pretry < 5:
password_input = await tab.find(
'input[autocomplete="current-password"]', timeout=15
)
await password_input.click()
await asyncio.sleep(0.5)
await password_input.send_keys(password)
await asyncio.sleep(0.2)
await password_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
pretry += 1
wait = pretry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Handle 2FA if needed
page_content = await tab.get_content()
if "verification code" in page_content or "Enter code" in page_content:
if not totp_seed:
raise Exception("2FA required but no TOTP seed provided")
print("[*] 2FA detected, entering code...", file=sys.stderr)
totp_code = pyotp.TOTP(totp_seed).now()
code_input = await tab.select('input[type="text"]')
await code_input.send_keys(totp_code + "\n")
await asyncio.sleep(3)
# Get cookies
print("[*] Retrieving cookies...", file=sys.stderr)
for _ in range(20): # 20 second timeout
cookies = await browser.cookies.get_all()
cookies_dict = {cookie.name: cookie.value for cookie in cookies}
if "auth_token" in cookies_dict and "ct0" in cookies_dict:
# Extract ID from twid cookie (may be URL-encoded)
user_id = None
if "twid" in cookies_dict:
twid = cookies_dict["twid"]
# Try to extract the ID from twid (format: u%3D<id> or u=<id>)
if "u%3D" in twid:
user_id = twid.split("u%3D")[1].split("&")[0].strip('"')
elif "u=" in twid:
user_id = twid.split("u=")[1].split("&")[0].strip('"')
cookies_dict["username"] = username
if user_id:
cookies_dict["id"] = user_id
return cookies_dict
await asyncio.sleep(1)
raise Exception("Timeout waiting for cookies")
finally:
browser.stop()
from create_session_browser import login_and_get_session
async def main():
if len(sys.argv) < 2:
print(
"Usage: python3 create_sessions_browser.py <accounts_file> [--append sessions.jsonl] [--headless]"
"Usage: python3 create_sessions_browser.py <accounts_file>"
" [--append sessions.jsonl] [--headless] [--delay N]"
)
sys.exit(1)
input = sys.argv[1]
input_file = sys.argv[1]
append_file = None
headless = False
delay = 1
delay = 3
# Parse optional arguments
i = 2
@ -162,7 +56,7 @@ async def main():
if arg == "--append":
if i + 1 < len(sys.argv):
append_file = sys.argv[i + 1]
i += 2 # Skip '--append' and filename
i += 2
else:
print("[!] Error: --append requires a filename", file=sys.stderr)
sys.exit(1)
@ -173,30 +67,28 @@ async def main():
delay = int(sys.argv[i + 1])
i += 2
else:
# Unkown args
print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr)
i += 1
accounts = []
with open(input) as f:
with open(input_file) as f:
accounts = json.load(f)
if len(accounts) == 0:
print("no accounts in file")
if not accounts:
print("No accounts in file")
sys.exit(0)
sessions = 0
for acc in accounts:
sessions += 1
ok, fail = [], []
for idx, acc in enumerate(accounts, 1):
username = acc["username"]
print(
f"\n[{idx}/{len(accounts)}] {username}...",
file=sys.stderr,
flush=True,
)
try:
cookies = await login_and_get_cookies(acc, headless)
session = {
"kind": "cookie",
"username": cookies["username"],
"id": cookies.get("id"),
"auth_token": cookies["auth_token"],
"ct0": cookies["ct0"],
}
session = await login_and_get_session(
username, acc["password"], acc.get("totp"), headless
)
if append_file:
with open(append_file, "a") as f:
@ -204,15 +96,30 @@ async def main():
else:
print(json.dumps(session))
print(f"Progress: {sessions} / {len(accounts)}")
if sessions < len(accounts):
print("Waiting", delay, "seconds")
sleep(delay)
except Exception as error:
ok.append(username)
print(
f"[!] Error getting session for {acc["username"]}, skipping: {error}",
f" ✓ saved (id={session['id']})",
file=sys.stderr,
flush=True,
)
except Exception as error:
fail.append(username)
print(
f"{error}",
file=sys.stderr,
flush=True,
)
if idx < len(accounts):
sleep(delay)
print(
f"\nDone: {len(ok)} ok, {len(fail)} failed",
file=sys.stderr,
flush=True,
)
if fail:
print(f" failed: {fail}", file=sys.stderr, flush=True)
if __name__ == "__main__":

View file

@ -1,3 +1,3 @@
nodriver>=0.48.0
zendriver>=0.15.0
pyotp
curl_cffi