Chromium: Russian language, autofill passwords from svc_login/svc_password via Login Data
This commit is contained in:
@@ -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
@@ -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,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user