137 lines
6.3 KiB
Python
Executable File
137 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
V99 Doctrine #100 - AUTO-LOGIN LinkedIn with stored creds
|
|
Never asks user. Uses LI_EMAIL + LI_PASSWORD + optional LI_TOTP_SEED from secrets.env.
|
|
Handles 2FA via pyotp. Saves cookies to persistent Chromium context.
|
|
"""
|
|
|
|
import asyncio, sys, subprocess, json, time
|
|
from pathlib import Path
|
|
|
|
SESSION_DIR = Path('/opt/weval-l99/browser-sessions/linkedin')
|
|
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
SCREENSHOT_DIR = Path('/var/www/html/api/playwright-results/v99-linkedin-auto-login')
|
|
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
LOG = Path('/var/log/v99-linkedin-auto-login.log')
|
|
|
|
def log(msg):
|
|
LOG.open('a').write(f"[{time.strftime('%Y-%m-%dT%H:%M:%S')}] {msg}\n")
|
|
|
|
def get_secret(key):
|
|
try:
|
|
r = subprocess.run(['grep', f'^{key}=', '/etc/weval/secrets.env'], capture_output=True, text=True, timeout=3)
|
|
if r.returncode == 0:
|
|
return r.stdout.strip().split('=', 1)[1].strip().strip('"').strip("'")
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
async def auto_login():
|
|
from playwright.async_api import async_playwright
|
|
|
|
email = get_secret('LI_EMAIL') or get_secret('LINKEDIN_EMAIL')
|
|
password = get_secret('LI_PASSWORD') or get_secret('LINKEDIN_PASSWORD')
|
|
totp_seed = get_secret('LI_TOTP_SEED') or get_secret('LINKEDIN_TOTP_SEED')
|
|
cookie = get_secret('LI_AT')
|
|
|
|
# Strategy 1: Direct cookie injection if provided
|
|
if cookie:
|
|
log("Strategy 1: injecting LI_AT cookie")
|
|
async with async_playwright() as p:
|
|
ctx = await p.chromium.launch_persistent_context(str(SESSION_DIR), headless=True)
|
|
await ctx.add_cookies([{
|
|
'name': 'li_at', 'value': cookie, 'domain': '.linkedin.com',
|
|
'path': '/', 'httpOnly': True, 'secure': True, 'sameSite': 'None',
|
|
}])
|
|
page = await ctx.new_page()
|
|
await page.goto('https://www.linkedin.com/feed/', wait_until='domcontentloaded', timeout=20000)
|
|
await page.wait_for_timeout(3000)
|
|
url = page.url
|
|
ok = 'login' not in url and 'checkpoint' not in url
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'cookie-inject.png'))
|
|
await ctx.close()
|
|
if ok:
|
|
log("Strategy 1 SUCCESS")
|
|
return {'ok': True, 'strategy': 'cookie_inject', 'url': url}
|
|
log(f"Strategy 1 FAILED url={url}")
|
|
|
|
# Strategy 2: Auto-login with email+password
|
|
if email and password:
|
|
log(f"Strategy 2: auto-login with {email}")
|
|
async with async_playwright() as p:
|
|
ctx = await p.chromium.launch_persistent_context(
|
|
str(SESSION_DIR), headless=True,
|
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
args=['--disable-blink-features=AutomationControlled'],
|
|
)
|
|
page = await ctx.new_page()
|
|
try:
|
|
await page.goto('https://www.linkedin.com/login', wait_until='domcontentloaded', timeout=20000)
|
|
await page.fill('input#username', email)
|
|
await page.fill('input#password', password)
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'autologin-1-form.png'))
|
|
await page.click('button[type="submit"]')
|
|
await page.wait_for_timeout(5000)
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'autologin-2-after-submit.png'))
|
|
|
|
# 2FA handling
|
|
url = page.url
|
|
if 'checkpoint/challenge' in url or 'verify' in url.lower():
|
|
if totp_seed:
|
|
try:
|
|
import pyotp
|
|
code = pyotp.TOTP(totp_seed).now()
|
|
log(f"2FA required, generated code {code[:2]}...")
|
|
for sel in ['input[name="pin"]', 'input#input__phone_verification_pin', 'input[type="text"]']:
|
|
try:
|
|
await page.fill(sel, code)
|
|
break
|
|
except: continue
|
|
await page.click('button[type="submit"]')
|
|
await page.wait_for_timeout(5000)
|
|
except ImportError:
|
|
log("pyotp not installed - 2FA blocked")
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'autologin-2fa-blocked.png'))
|
|
await ctx.close()
|
|
return {'ok': False, 'err': 'pyotp_missing', 'strategy': 'autologin'}
|
|
else:
|
|
log("2FA required but no TOTP_SEED")
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'autologin-2fa-no-seed.png'))
|
|
|
|
await page.goto('https://www.linkedin.com/feed/', wait_until='domcontentloaded', timeout=15000)
|
|
await page.wait_for_timeout(3000)
|
|
url = page.url
|
|
ok = 'login' not in url and 'checkpoint' not in url
|
|
await page.screenshot(path=str(SCREENSHOT_DIR / 'autologin-3-feed.png'))
|
|
await ctx.close()
|
|
if ok:
|
|
log("Strategy 2 SUCCESS")
|
|
return {'ok': True, 'strategy': 'auto_login_creds', 'url': url}
|
|
log(f"Strategy 2 FAILED url={url}")
|
|
return {'ok': False, 'strategy': 'autologin', 'url': url, 'err': 'final_check_failed'}
|
|
except Exception as e:
|
|
log(f"Strategy 2 EXCEPTION {e}")
|
|
await ctx.close()
|
|
return {'ok': False, 'err': str(e)[:200], 'strategy': 'autologin'}
|
|
|
|
# All strategies exhausted
|
|
log("All strategies exhausted - no creds configured")
|
|
return {
|
|
'ok': False,
|
|
'err': 'NO_CREDS_CONFIGURED',
|
|
'missing': {
|
|
'LI_AT (cookie)': cookie is None,
|
|
'LI_EMAIL': email is None,
|
|
'LI_PASSWORD': password is None,
|
|
'LI_TOTP_SEED (optional for 2FA)': totp_seed is None,
|
|
},
|
|
'instruction': 'Admin: add LI_EMAIL + LI_PASSWORD to /etc/weval/secrets.env (ONE time) for future auto-login. Optional: LI_TOTP_SEED if 2FA enabled.',
|
|
}
|
|
|
|
async def main():
|
|
r = await auto_login()
|
|
print(json.dumps(r, indent=2))
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|