From 23c1f6e342ffa4b2b112932eded7ff1a6b9c0833 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Thu, 30 Apr 2026 07:22:31 +0000 Subject: [PATCH] Chromium: Russian language, autofill passwords from svc_login/svc_password via Login Data --- app/main.py | 4 +- universal-runtime/Dockerfile | 1 + universal-runtime/manager.py | 160 ++++++++++++++++++++++++++++++----- 3 files changed, 143 insertions(+), 22 deletions(-) diff --git a/app/main.py b/app/main.py index 766be04..110a3b4 100644 --- a/app/main.py +++ b/app/main.py @@ -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 diff --git a/universal-runtime/Dockerfile b/universal-runtime/Dockerfile index 75eff12..692025a 100644 --- a/universal-runtime/Dockerfile +++ b/universal-runtime/Dockerfile @@ -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 diff --git a/universal-runtime/manager.py b/universal-runtime/manager.py index 69a6b6c..bcff40e 100644 --- a/universal-runtime/manager.py +++ b/universal-runtime/manager.py @@ -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, {