#!/usr/bin/env python3 """Patch a disposable engine and execute its actual autofill JS without Qt/browser.""" import ast import json import os from pathlib import Path import subprocess import sys import tempfile import tomllib from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[1] SMOKE = (ROOT/'tests/smoke.sh').read_text() with tempfile.TemporaryDirectory(prefix='lemana-auth-retry-') as temporary: folder = Path(temporary) for name in ('webengine', 'authenticator'): fixture = SMOKE.split('cat > "$fake_'+name+'" <<\'PY\'\n', 1)[1].split('\nPY\n', 1)[0] (folder/(name+'.py')).write_text(fixture) env = {key:value for key,value in os.environ.items() if not key.startswith(('LEMANA_VPN_', 'OPENCONNECT_LITE_'))} env.update(HOME=temporary, LEMANA_VPN_WEBENGINE_PROCESS=str(folder/'webengine.py'), LEMANA_VPN_AUTHENTICATOR=str(folder/'authenticator.py'), LEMANA_VPN_OC_PYTHON=sys.executable, LEMANA_VPN_PATCH_BACKUP_DIR=str(folder/'backups')) command = ['bash', str(ROOT/'bin/vpn-lemanapro.sh'), '--patch-only'] subprocess.run(command, env=env, check=True, capture_output=True) patched = (folder/'webengine.py').read_text() subprocess.run(command, env=env, check=True, capture_output=True) assert patched == (folder/'webengine.py').read_text(), 'Patch must be idempotent' compile(patched, '', 'exec') tree = ast.parse(patched) selectors = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'get_selectors') template = next(n.args[0] for n in ast.walk(tree) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr == 'setSourceCode') profile = tomllib.loads((ROOT/'templates/openconnect-lite-config.toml').read_text()) url_pattern, rule_data = next(iter(profile['auto_fill_rules'].items())) rules = [SimpleNamespace(selector=r['selector'], action=r.get('action'), fill=r.get('fill')) for r in rule_data] messages, exits, delegated = [], [], [] class BasePage: def javaScriptConsoleMessage(self, *args): delegated.append(args) logger = SimpleNamespace(warning=lambda *args, **kwargs: messages.append(args)) fake_os = SimpleNamespace(environ={}) namespace = dict(os=fake_os, json=json, logger=logger, QWebEnginePage=BasePage, QApplication=SimpleNamespace(instance=lambda: SimpleNamespace(exit=exits.append)), credentials=SimpleNamespace(username='test-user', password='test-password', totp='123456'), rules=rules, url_pattern=url_pattern, js_regex_str=json.dumps('^'+url_pattern.replace('.', '\\.').replace('*', '.*')+'$')) callback = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'LemanaVPNPage') exec(compile(ast.Module(body=[selectors, callback], type_ignores=[]), '', 'exec'), namespace) page = namespace['LemanaVPNPage']() page.javaScriptConsoleMessage(1, 'LEMANA_VPN_AUTH_REJECTED', 0, 'test') assert exits == [77] and messages == [('LEMANA_VPN_AUTH_REJECTED',)] fake_os.environ['LEMANA_VPN_AUTOFILL_CLICK'] = '0' page.javaScriptConsoleMessage(1, 'LEMANA_VPN_AUTH_REJECTED', 0, 'test') assert exits == [77] and len(delegated) == 1 scripts = {} for name, click, attempt in [('auto', '1', 'attempt-one'), ('fresh', '1', 'attempt-two'), ('manual', '0', 'attempt-one')]: fake_os.environ.update(LEMANA_VPN_AUTOFILL_CLICK=click, LEMANA_VPN_AUTH_ATTEMPT=attempt, LEMANA_VPN_AUTOFILL_TOTP='0' if name == 'manual' else '1') scripts[name] = eval(compile(ast.Expression(template), '