Improve autofill extension: better field detection + Basic Auth support

- Split filled flag into userFilled/passFilled for independent tracking
- Add findUserFieldNearPassword() for DOM-relative lookup near password field
- Add isVisible() helper to skip disabled/hidden/offscreen inputs
- Add console.log tracing for debugging
- Add background.js service worker with webRequest.onAuthRequired for Basic Auth
- Add _url_with_credentials() to embed login:pass in URL for HTTP Basic Auth
- Use /usr/lib/chromium/chromium binary directly (bypass Debian wrapper)
- Add --enable-logging=stderr for console.log capture in chromium logs
This commit is contained in:
2026-05-01 04:44:15 +00:00
parent d57acb416b
commit 9bd38ed6db
+118 -24
View File
@@ -7,6 +7,7 @@ import subprocess
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, urlunparse, quote
DISPLAY = os.environ.get("DISPLAY", ":1")
CHROME_WINDOW_SIZE = os.environ.get("CHROME_WINDOW_SIZE", "1920,1080")
@@ -29,33 +30,71 @@ _lock = threading.Lock()
_AUTOFILL_CONTENT_JS = r"""
(function() {
const CREDS = __CREDS__;
let filled = false;
let userFilled = false;
let passFilled = false;
console.log('[PortalAutofill] loaded for', location.href);
function findUserField() {
function isVisible(el) {
if (!el) return false;
if (el.disabled || el.readOnly) return false;
if (el.offsetParent === null && el.type !== 'email') return false;
return true;
}
function findUserFieldByAttrs() {
const candidates = document.querySelectorAll(
'input[type="email"], ' +
'input[autocomplete*="username"], ' +
'input[autocomplete*="email"], ' +
'input[name*="user" i], ' +
'input[name*="login" i], ' +
'input[name*="email" i], ' +
'input[id*="user" i], ' +
'input[id*="login" i], ' +
'input[id*="email" i], ' +
'input[type="text"]'
'input[name*="user" i]:not([type="password"]):not([type="hidden"]), ' +
'input[name*="login" i]:not([type="password"]):not([type="hidden"]), ' +
'input[name*="email" i]:not([type="password"]):not([type="hidden"]), ' +
'input[id*="user" i]:not([type="password"]):not([type="hidden"]), ' +
'input[id*="login" i]:not([type="password"]):not([type="hidden"]), ' +
'input[id*="email" i]:not([type="password"]):not([type="hidden"])'
);
for (const el of candidates) {
if (el.type === 'password' || el.type === 'hidden') continue;
if (el.offsetParent === null && el.type !== 'email') continue;
return el;
if (isVisible(el)) return el;
}
return null;
}
function findUserFieldNearPassword(passEl) {
if (!passEl) return null;
// Walk up to find the closest form-like container, then look for
// any text/email input that comes BEFORE the password element.
let container = passEl.closest('form');
if (!container) {
let cur = passEl.parentElement;
while (cur && cur !== document.body) {
if (cur.querySelectorAll('input').length >= 2) { container = cur; break; }
cur = cur.parentElement;
}
}
if (!container) container = document.body;
const inputs = container.querySelectorAll('input');
let candidate = null;
for (const el of inputs) {
if (el === passEl) break;
const t = (el.type || 'text').toLowerCase();
if (t === 'password' || t === 'hidden' || t === 'submit' || t === 'button' ||
t === 'checkbox' || t === 'radio' || t === 'file') continue;
if (!isVisible(el)) continue;
candidate = el;
}
return candidate;
}
function findUserField(passEl) {
return findUserFieldByAttrs() || findUserFieldNearPassword(passEl) ||
Array.from(document.querySelectorAll('input[type="text"], input:not([type])'))
.find(isVisible) || null;
}
function findPassField() {
const list = document.querySelectorAll('input[type="password"]');
for (const el of list) {
if (el.offsetParent !== null) return el;
if (isVisible(el)) return el;
}
return list[0] || null;
}
@@ -73,14 +112,23 @@ _AUTOFILL_CONTENT_JS = r"""
}
function tryFill() {
if (filled) return;
if (userFilled && passFilled) return;
const p = findPassField();
if (!p) return;
const u = findUserField();
let did = false;
if (CREDS.login && u) did = setNativeValue(u, CREDS.login) || did;
if (CREDS.password) did = setNativeValue(p, CREDS.password) || did;
if (did) filled = true;
const u = findUserField(p);
if (CREDS.password && p && !passFilled) {
if (setNativeValue(p, CREDS.password)) {
passFilled = true;
console.log('[PortalAutofill] password filled');
}
}
if (CREDS.login && u && !userFilled) {
if (setNativeValue(u, CREDS.login)) {
userFilled = true;
console.log('[PortalAutofill] user filled');
}
}
if (!CREDS.login) userFilled = true;
if (!CREDS.password) passFilled = true;
}
if (document.readyState === 'loading') {
@@ -89,12 +137,18 @@ _AUTOFILL_CONTENT_JS = r"""
tryFill();
}
const obs = new MutationObserver(() => { if (!filled) tryFill(); });
const obs = new MutationObserver(() => {
if (!(userFilled && passFilled)) tryFill();
});
if (document.documentElement) {
obs.observe(document.documentElement, { childList: true, subtree: true });
}
const resetAndRefill = () => { filled = false; setTimeout(tryFill, 150); };
const resetAndRefill = () => {
userFilled = !CREDS.login;
passFilled = !CREDS.password;
setTimeout(tryFill, 150);
};
['pushState', 'replaceState'].forEach(fn => {
const orig = history[fn];
history[fn] = function() { const r = orig.apply(this, arguments); resetAndRefill(); return r; };
@@ -108,6 +162,9 @@ _AUTOFILL_MANIFEST = {
"name": "Portal Autofill",
"version": "1.0",
"description": "Auto-fill credentials for portal session",
"background": {"service_worker": "background.js"},
"permissions": ["webRequest"],
"host_permissions": ["<all_urls>"],
"content_scripts": [
{
"matches": ["<all_urls>"],
@@ -118,6 +175,19 @@ _AUTOFILL_MANIFEST = {
],
}
_AUTOFILL_BACKGROUND_JS = r"""
const CREDS = __CREDS__;
if (CREDS.login || CREDS.password) {
chrome.webRequest.onAuthRequired.addListener(
function(details, callback) {
callback({ authCredentials: { username: CREDS.login || '', password: CREDS.password || '' } });
},
{ urls: ['<all_urls>'] },
['asyncBlocking']
);
}
"""
def _create_autofill_extension(login: str, password: str) -> str | None:
if not login and not password:
@@ -125,10 +195,13 @@ def _create_autofill_extension(login: str, password: str) -> str | None:
ext_dir = tempfile.mkdtemp(prefix="chrome-autofill-ext-")
creds_json = json.dumps({"login": login, "password": password})
content_js = _AUTOFILL_CONTENT_JS.replace("__CREDS__", creds_json)
background_js = _AUTOFILL_BACKGROUND_JS.replace("__CREDS__", creds_json)
with open(os.path.join(ext_dir, "manifest.json"), "w") as f:
json.dump(_AUTOFILL_MANIFEST, f)
with open(os.path.join(ext_dir, "content.js"), "w") as f:
f.write(content_js)
with open(os.path.join(ext_dir, "background.js"), "w") as f:
f.write(background_js)
return ext_dir
@@ -257,6 +330,18 @@ def apply_resolution(width: int | None, height: int | None) -> tuple[int, int]:
return safe_w, safe_h
def _url_with_credentials(url: str, login: str, password: str) -> str:
"""Embed login:password into URL so Chromium auto-handles Basic Auth."""
if not login and not password:
return url
parsed = urlparse(url)
netloc = parsed.hostname or ""
if parsed.port:
netloc += f":{parsed.port}"
user_info = quote(login, safe="") + ":" + quote(password, safe="")
return urlunparse(parsed._replace(netloc=f"{user_info}@{netloc}"))
def open_web(
url: str,
width: int | None = None,
@@ -267,9 +352,16 @@ def open_web(
safe_w, safe_h = apply_resolution(width, height)
profile_dir = _create_chrome_profile()
extension_dir = _create_autofill_extension(login, password)
# Embed credentials in URL for HTTP Basic Auth (no dialog shown)
url_with_creds = _url_with_credentials(url, login, password)
# Use the real Chromium binary directly to avoid the Debian wrapper which
# injects an empty `--load-extension=` from /etc/chromium.d/extensions.
chromium_bin = "/usr/lib/chromium/chromium"
if not os.path.isfile(chromium_bin):
chromium_bin = "chromium"
cmd = [
"chromium",
chromium_bin,
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
@@ -285,6 +377,8 @@ def open_web(
f"--window-size={safe_w},{safe_h}",
"--no-first-run",
"--no-default-browser-check",
"--enable-logging=stderr",
"--v=0",
"--lang=ru-RU",
"--accept-lang=ru-RU,ru",
"--password-store=basic",
@@ -293,7 +387,7 @@ def open_web(
if extension_dir:
cmd.append(f"--load-extension={extension_dir}")
cmd.append(f"--disable-extensions-except={extension_dir}")
cmd.append(url)
cmd.append(url_with_creds)
_start_process(cmd, "web", url)
_state["profile_dir"] = profile_dir