Chromium: Russian language, autofill passwords from svc_login/svc_password via Login Data

This commit is contained in:
2026-04-30 07:22:31 +00:00
parent d7c956e10b
commit 23c1f6e342
3 changed files with 143 additions and 22 deletions
+2 -2
View File
@@ -840,7 +840,7 @@ def dispatch_universal_target(slot: int, service: Service, width: Optional[int]
payload = {}
if service.type == ServiceType.WEB:
url = f"http://{name}:7000/open"
payload = {"url": normalize_web_target(service.target)}
payload = {"url": normalize_web_target(service.target), "login": service.svc_login or "", "password": service.svc_password or ""}
width, height = sanitize_client_resolution(width, height)
if width and height:
payload["width"] = width
@@ -876,7 +876,7 @@ def dispatch_web_pool_target(slot: int, service: Service, width: Optional[int] =
name = web_pool_container_name(slot)
target_url = normalize_web_target(service.target)
url = f"http://{name}:7000/open"
payload = {"url": target_url}
payload = {"url": target_url, "login": service.svc_login or "", "password": service.svc_password or ""}
width, height = sanitize_client_resolution(width, height)
if width and height:
payload["width"] = width
+1
View File
@@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
x11-xserver-utils \
x11-utils \
fonts-dejavu-core \
python3-cryptography \
&& rm -rf /var/lib/apt/lists/*
COPY entrypoint.sh /entrypoint.sh
+140 -20
View File
@@ -1,10 +1,16 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import shutil
import signal
import sqlite3
import subprocess
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
DISPLAY = os.environ.get("DISPLAY", ":1")
CHROME_WINDOW_SIZE = os.environ.get("CHROME_WINDOW_SIZE", "1920,1080")
@@ -18,24 +24,117 @@ _state = {
"mode": "idle",
"target": "",
"resolution": CHROME_WINDOW_SIZE,
"profile_dir": None,
}
_lock = threading.Lock()
def _chrome_encrypt_v10(plaintext: str) -> bytes:
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
except ImportError:
return plaintext.encode("utf-8")
key = hashlib.pbkdf2_hmac("sha1", b"peanuts", b"saltysalt", 1, dklen=16)
iv = b" " * 16
data = plaintext.encode("utf-8")
pad = 16 - (len(data) % 16)
data += bytes([pad] * pad)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
enc = cipher.encryptor()
return b"v10" + enc.update(data) + enc.finalize()
def _create_chrome_profile(login: str, password: str, url: str) -> str:
profile_dir = tempfile.mkdtemp(prefix="chrome-profile-")
default_dir = os.path.join(profile_dir, "Default")
os.makedirs(default_dir, exist_ok=True)
prefs = {
"intl": {"accept_languages": "ru-RU,ru,en", "selected_languages": "ru-RU,ru"},
"translate": {"enabled": False},
"translate_blocked_languages": ["ru"],
"credentials_enable_service": True,
"credentials_enable_autosign_in": False,
}
with open(os.path.join(default_dir, "Preferences"), "w") as f:
json.dump(prefs, f)
if not login and not password:
return profile_dir
parsed = urlparse(url)
origin = f"{parsed.scheme}://{parsed.netloc}/"
db_path = os.path.join(default_dir, "Login Data")
conn = sqlite3.connect(db_path)
conn.execute(
"CREATE TABLE IF NOT EXISTS meta "
"(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR)"
)
conn.execute("INSERT OR REPLACE INTO meta VALUES ('version', '30')")
conn.execute("INSERT OR REPLACE INTO meta VALUES ('last_compatible_version', '30')")
conn.execute("""
CREATE TABLE IF NOT EXISTS logins (
origin_url VARCHAR NOT NULL,
action_url VARCHAR,
username_element VARCHAR,
username_value VARCHAR,
password_element VARCHAR,
password_value BLOB,
submit_element VARCHAR,
signon_realm VARCHAR NOT NULL,
date_created INTEGER NOT NULL,
blacklisted_by_user INTEGER NOT NULL,
scheme INTEGER NOT NULL,
password_type INTEGER DEFAULT 0,
times_used INTEGER DEFAULT 0,
form_data BLOB DEFAULT '',
display_name VARCHAR DEFAULT '',
icon_url VARCHAR DEFAULT '',
federation_url VARCHAR DEFAULT '',
skip_zero_click INTEGER DEFAULT 0,
generation_upload_status INTEGER DEFAULT 0,
possible_username_pairs BLOB DEFAULT '',
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_last_used INTEGER DEFAULT 0,
moving_blocked_for BLOB DEFAULT '',
date_password_modified INTEGER DEFAULT 0
)""")
enc_password = _chrome_encrypt_v10(password) if password else b""
now = int(time.time() * 1_000_000)
conn.execute(
"INSERT INTO logins "
"(origin_url, action_url, username_element, username_value, "
"password_element, password_value, submit_element, signon_realm, "
"date_created, blacklisted_by_user, scheme, times_used, date_last_used) "
"VALUES (?,?,?,?,?,?,?,?,?,0,0,1,?)",
(origin, origin, "", login, "", enc_password, "", origin, now, now),
)
conn.commit()
conn.close()
return profile_dir
def _stop_current() -> None:
proc = _state.get("proc")
if not proc:
return
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
proc.wait(timeout=4)
except Exception:
if proc:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
proc.wait(timeout=4)
except Exception:
pass
finally:
_state["proc"] = None
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except Exception:
pass
finally:
_state["proc"] = None
profile_dir = _state.get("profile_dir")
if profile_dir and os.path.isdir(profile_dir):
shutil.rmtree(profile_dir, ignore_errors=True)
_state["profile_dir"] = None
def _start_process(cmd: list[str], mode: str, target: str) -> None:
@@ -62,7 +161,6 @@ def _sanitize_resolution(width: int | None, height: int | None) -> tuple[int, in
return default_w, default_h
except Exception:
return 1920, 1080
safe_w = max(RESOLUTION_MIN_WIDTH, min(int(width), RESOLUTION_MAX_WIDTH))
safe_h = max(RESOLUTION_MIN_HEIGHT, min(int(height), RESOLUTION_MAX_HEIGHT))
return safe_w, safe_h
@@ -90,18 +188,26 @@ def _add_mode_via_cvt(width: int, height: int, output_name: str) -> bool:
)
if cvt.returncode != 0:
return False
modeline_line = next((l for l in cvt.stdout.splitlines() if l.startswith("Modeline")), None)
modeline_line = next(
(l for l in cvt.stdout.splitlines() if l.startswith("Modeline")), None
)
if not modeline_line:
return False
parts = modeline_line.split()
mode_name = parts[1].strip('"')
mode_params = parts[2:]
subprocess.run(["xrandr", "-display", DISPLAY, "--newmode", mode_name] + mode_params,
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["xrandr", "-display", DISPLAY, "--addmode", output_name, mode_name],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["xrandr", "-display", DISPLAY, "--output", output_name, "--mode", mode_name],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(
["xrandr", "-display", DISPLAY, "--newmode", mode_name] + mode_params,
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
subprocess.run(
["xrandr", "-display", DISPLAY, "--addmode", output_name, mode_name],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
subprocess.run(
["xrandr", "-display", DISPLAY, "--output", output_name, "--mode", mode_name],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
return True
except Exception:
return False
@@ -121,8 +227,16 @@ def apply_resolution(width: int | None, height: int | None) -> tuple[int, int]:
return safe_w, safe_h
def open_web(url: str, width: int | None = None, height: int | None = None) -> None:
def open_web(
url: str,
width: int | None = None,
height: int | None = None,
login: str = "",
password: str = "",
) -> None:
safe_w, safe_h = apply_resolution(width, height)
profile_dir = _create_chrome_profile(login, password, url)
_state["profile_dir"] = profile_dir
cmd = [
"chromium",
"--no-sandbox",
@@ -140,6 +254,10 @@ def open_web(url: str, width: int | None = None, height: int | None = None) -> N
f"--window-size={safe_w},{safe_h}",
"--no-first-run",
"--no-default-browser-check",
"--lang=ru-RU",
"--accept-lang=ru-RU,ru",
"--password-store=basic",
f"--user-data-dir={profile_dir}",
url,
]
_start_process(cmd, "web", url)
@@ -220,8 +338,10 @@ class Handler(BaseHTTPRequestHandler):
return
width = data.get("width")
height = data.get("height")
login = (data.get("login") or "").strip()
password = (data.get("password") or "").strip()
with _lock:
open_web(url, width=width, height=height)
open_web(url, width=width, height=height, login=login, password=password)
self._json(
200,
{