This commit is contained in:
StellaOps Bot
2025-12-09 00:20:52 +02:00
parent 3d01bf9edc
commit bc0762e97d
261 changed files with 14033 additions and 4427 deletions

View File

@@ -80,6 +80,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
WINE_CSP_MODE=limited \
WINE_CSP_INSTALLER_PATH=/opt/cryptopro/csp-installer.msi \
WINE_CSP_LOG_LEVEL=Information \
NODE_PATH=/usr/local/lib/node_modules \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
# Display for Wine (headless)
DISPLAY=:99
@@ -117,6 +119,21 @@ RUN set -eux; \
apt-get clean; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Node.js + Playwright (headless Chromium) for CryptoPro downloader
RUN set -eux; \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get update; \
apt-get install -y --no-install-recommends \
nodejs \
rpm2cpio \
cpio; \
npm install -g --no-progress playwright-chromium@1.48.2; \
npx playwright install-deps chromium; \
npx playwright install chromium; \
chown -R ${APP_UID}:${APP_GID} /ms-playwright || true; \
apt-get clean; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Create non-root user for Wine service
# Note: Wine requires writable home directory for prefix
RUN groupadd -r -g ${APP_GID} ${APP_USER} && \
@@ -133,7 +150,10 @@ COPY --from=build --chown=${APP_UID}:${APP_GID} /app/publish/ ./
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/healthcheck.sh /usr/local/bin/healthcheck.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/install-csp.sh /usr/local/bin/install-csp.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/install-csp.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/fetch-cryptopro.py /usr/local/bin/fetch-cryptopro.py
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/download-cryptopro.sh /usr/local/bin/download-cryptopro.sh
COPY --chown=${APP_UID}:${APP_GID} scripts/crypto/download-cryptopro-playwright.cjs /usr/local/bin/download-cryptopro-playwright.cjs
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/install-csp.sh /usr/local/bin/fetch-cryptopro.py /usr/local/bin/download-cryptopro.sh /usr/local/bin/download-cryptopro-playwright.cjs
# Switch to non-root user for Wine prefix initialization
USER ${APP_UID}:${APP_GID}

View File

@@ -0,0 +1,62 @@
#!/bin/bash
# CryptoPro Linux package fetcher (Playwright-driven)
# Uses the Node-based Playwright crawler to authenticate (if required) and
# download Linux CSP installers. Intended to run once per container startup.
set -euo pipefail
OUTPUT_DIR="${CRYPTOPRO_OUTPUT_DIR:-/opt/cryptopro/downloads}"
MARKER="${CRYPTOPRO_DOWNLOAD_MARKER:-${OUTPUT_DIR}/.downloaded}"
FORCE="${CRYPTOPRO_FORCE_DOWNLOAD:-0}"
UNPACK="${CRYPTOPRO_UNPACK:-1}"
DRY_RUN="${CRYPTOPRO_DRY_RUN:-1}"
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [crypto-fetch] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [crypto-fetch] [ERROR] $*" >&2
}
if [[ -f "${MARKER}" && "${FORCE}" != "1" ]]; then
log "Download marker present at ${MARKER}; skipping (set CRYPTOPRO_FORCE_DOWNLOAD=1 to refresh)."
exit 0
fi
log "Ensuring CryptoPro Linux packages are available (dry-run unless CRYPTOPRO_DRY_RUN=0)"
log " Output dir: ${OUTPUT_DIR}"
log " Unpack: ${UNPACK}"
mkdir -p "${OUTPUT_DIR}"
# Export defaults for the Playwright downloader
export CRYPTOPRO_OUTPUT_DIR="${OUTPUT_DIR}"
export CRYPTOPRO_UNPACK="${UNPACK}"
export CRYPTOPRO_DRY_RUN="${DRY_RUN}"
export CRYPTOPRO_URL="${CRYPTOPRO_URL:-https://cryptopro.ru/products/csp/downloads#latest_csp50r3_linux}"
export CRYPTOPRO_EMAIL="${CRYPTOPRO_EMAIL:-contact@stella-ops.org}"
export CRYPTOPRO_PASSWORD="${CRYPTOPRO_PASSWORD:-Hoko33JD3nj3aJD.}"
if ! node /usr/local/bin/download-cryptopro-playwright.cjs; then
rc=$?
if [[ "${rc}" == "2" ]]; then
log "Playwright downloader blocked by auth/captcha; skipping download (set CRYPTOPRO_DEBUG=1 for details)."
exit 0
fi
log_error "Playwright downloader failed (exit=${rc})"
exit "${rc}"
fi
if [[ "${DRY_RUN}" == "0" ]]; then
touch "${MARKER}"
log "Download complete; marker written to ${MARKER}"
else
log "Dry-run mode; marker not written. Set CRYPTOPRO_DRY_RUN=0 to fetch binaries."
fi
# List latest artifacts (best-effort)
if compgen -G "${OUTPUT_DIR}/*" > /dev/null; then
log "Artifacts in ${OUTPUT_DIR}:"
find "${OUTPUT_DIR}" -maxdepth 1 -type f -printf " %f (%s bytes)\n" | head -20
fi

View File

@@ -15,6 +15,10 @@ WINE_CSP_INSTALLER_PATH="${WINE_CSP_INSTALLER_PATH:-/opt/cryptopro/csp-installer
WINE_CSP_LOG_LEVEL="${WINE_CSP_LOG_LEVEL:-Information}"
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
DISPLAY="${DISPLAY:-:99}"
CSP_DOWNLOAD_MARKER="${WINE_CSP_INSTALLER_PATH}.downloaded"
CRYPTOPRO_DOWNLOAD_DIR="${CRYPTOPRO_DOWNLOAD_DIR:-/opt/cryptopro/downloads}"
CRYPTOPRO_DOWNLOAD_MARKER="${CRYPTOPRO_DOWNLOAD_MARKER:-${CRYPTOPRO_DOWNLOAD_DIR}/.downloaded}"
CRYPTOPRO_FETCH_ON_START="${CRYPTOPRO_FETCH_ON_START:-1}"
# Marker files
CSP_INSTALLED_MARKER="${WINE_PREFIX}/.csp_installed"
@@ -73,6 +77,37 @@ initialize_wine() {
log "Wine prefix initialized successfully"
}
# ------------------------------------------------------------------------------
# CryptoPro Linux Downloads (Playwright-driven)
# ------------------------------------------------------------------------------
download_linux_packages() {
if [[ "${CRYPTOPRO_FETCH_ON_START}" == "0" ]]; then
log "Skipping CryptoPro Linux fetch (CRYPTOPRO_FETCH_ON_START=0)"
return 0
fi
if [[ -f "${CRYPTOPRO_DOWNLOAD_MARKER}" && "${CRYPTOPRO_FORCE_DOWNLOAD:-0}" != "1" ]]; then
log "CryptoPro download marker present at ${CRYPTOPRO_DOWNLOAD_MARKER}; skipping fetch"
return 0
fi
log "Ensuring CryptoPro Linux packages via Playwright (dry-run unless CRYPTOPRO_DRY_RUN=0)"
export CRYPTOPRO_DOWNLOAD_MARKER
export CRYPTOPRO_OUTPUT_DIR="${CRYPTOPRO_DOWNLOAD_DIR}"
export CRYPTOPRO_UNPACK="${CRYPTOPRO_UNPACK:-1}"
if /usr/local/bin/download-cryptopro.sh; then
if [[ "${CRYPTOPRO_DRY_RUN:-1}" != "0" ]]; then
log "CryptoPro downloader ran in dry-run mode; set CRYPTOPRO_DRY_RUN=0 to fetch binaries"
else
[[ -f "${CRYPTOPRO_DOWNLOAD_MARKER}" ]] || touch "${CRYPTOPRO_DOWNLOAD_MARKER}"
log "CryptoPro Linux artifacts staged in ${CRYPTOPRO_DOWNLOAD_DIR}"
fi
else
log_error "CryptoPro Playwright download failed"
fi
}
# ------------------------------------------------------------------------------
# CryptoPro CSP Installation
# ------------------------------------------------------------------------------
@@ -83,6 +118,15 @@ install_cryptopro() {
return 0
fi
# Attempt to download installer if missing (dry-run by default)
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log "CryptoPro CSP installer not found at ${WINE_CSP_INSTALLER_PATH}; attempting crawl/download (dry-run unless CRYPTOPRO_DRY_RUN=0)."
if ! CRYPTOPRO_OUTPUT="${WINE_CSP_INSTALLER_PATH}" /usr/local/bin/fetch-cryptopro.py; then
log_error "CryptoPro CSP download failed; continuing without CSP (limited mode)"
return 0
fi
fi
# Check if installer is available
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log "CryptoPro CSP installer not found at ${WINE_CSP_INSTALLER_PATH}"
@@ -201,6 +245,7 @@ main() {
log "=========================================="
validate_environment
download_linux_packages
initialize_wine
# Only attempt CSP installation in full mode

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
CryptoPro crawler (metadata only by default).
Fetches https://cryptopro.ru/downloads (or override) with basic auth, recurses linked pages,
and selects candidate Linux packages (.deb/.rpm/.tar.gz/.tgz/.run) or MSI as fallback.
Environment:
CRYPTOPRO_DOWNLOAD_URL: start URL (default: https://cryptopro.ru/downloads)
CRYPTOPRO_USERNAME / CRYPTOPRO_PASSWORD: credentials
CRYPTOPRO_MAX_PAGES: max pages to crawl (default: 20)
CRYPTOPRO_MAX_DEPTH: max link depth (default: 2)
CRYPTOPRO_DRY_RUN: 1 (default) to list only, 0 to enable download
CRYPTOPRO_OUTPUT: output path (default: /opt/cryptopro/csp-installer.bin)
"""
import os
import sys
import re
import html.parser
import urllib.parse
import urllib.request
from collections import deque
SESSION_HEADERS = {
"User-Agent": "StellaOps-CryptoPro-Crawler/1.0 (+https://stella-ops.org)",
}
LINUX_PATTERNS = re.compile(r"\.(deb|rpm|tar\.gz|tgz|run)(?:$|\?)", re.IGNORECASE)
MSI_PATTERN = re.compile(r"\.msi(?:$|\?)", re.IGNORECASE)
def log(msg: str) -> None:
sys.stdout.write(msg + "\n")
sys.stdout.flush()
def warn(msg: str) -> None:
sys.stderr.write("[WARN] " + msg + "\n")
sys.stderr.flush()
class LinkParser(html.parser.HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag != "a":
return
href = dict(attrs).get("href")
if href:
self.links.append(href)
def fetch(url: str, auth_handler) -> tuple[str, list[str]]:
opener = urllib.request.build_opener(auth_handler)
req = urllib.request.Request(url, headers=SESSION_HEADERS)
with opener.open(req, timeout=30) as resp:
data = resp.read()
parser = LinkParser()
parser.feed(data.decode("utf-8", errors="ignore"))
return data, parser.links
def resolve_links(base: str, links: list[str]) -> list[str]:
resolved = []
for href in links:
if href.startswith("#") or href.startswith("mailto:"):
continue
resolved.append(urllib.parse.urljoin(base, href))
return resolved
def choose_candidates(urls: list[str]) -> tuple[list[str], list[str]]:
linux = []
msi = []
for u in urls:
if LINUX_PATTERNS.search(u):
linux.append(u)
elif MSI_PATTERN.search(u):
msi.append(u)
# stable ordering
linux = sorted(set(linux))
msi = sorted(set(msi))
return linux, msi
def download(url: str, output_path: str, auth_handler) -> int:
opener = urllib.request.build_opener(auth_handler)
req = urllib.request.Request(url, headers=SESSION_HEADERS)
with opener.open(req, timeout=60) as resp:
with open(output_path, "wb") as f:
f.write(resp.read())
return os.path.getsize(output_path)
def main() -> int:
start_url = os.environ.get("CRYPTOPRO_DOWNLOAD_URL", "https://cryptopro.ru/downloads")
username = os.environ.get("CRYPTOPRO_USERNAME", "contact@stella-ops.org")
password = os.environ.get("CRYPTOPRO_PASSWORD", "Hoko33JD3nj3aJD.")
max_pages = int(os.environ.get("CRYPTOPRO_MAX_PAGES", "20"))
max_depth = int(os.environ.get("CRYPTOPRO_MAX_DEPTH", "2"))
dry_run = os.environ.get("CRYPTOPRO_DRY_RUN", "1") != "0"
output_path = os.environ.get("CRYPTOPRO_OUTPUT", "/opt/cryptopro/csp-installer.bin")
if username == "contact@stella-ops.org" and password == "Hoko33JD3nj3aJD.":
warn("Using default demo credentials; set CRYPTOPRO_USERNAME/CRYPTOPRO_PASSWORD to real customer creds.")
passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
passman.add_password(None, start_url, username, password)
auth_handler = urllib.request.HTTPBasicAuthHandler(passman)
seen = set()
queue = deque([(start_url, 0)])
crawled = 0
all_links = []
while queue and crawled < max_pages:
url, depth = queue.popleft()
if url in seen or depth > max_depth:
continue
seen.add(url)
try:
data, links = fetch(url, auth_handler)
crawled += 1
log(f"[crawl] {url} ({len(data)} bytes, depth={depth}, links={len(links)})")
except Exception as ex: # noqa: BLE001
warn(f"[crawl] failed {url}: {ex}")
continue
resolved = resolve_links(url, links)
all_links.extend(resolved)
for child in resolved:
if child not in seen and depth + 1 <= max_depth:
queue.append((child, depth + 1))
linux, msi = choose_candidates(all_links)
log(f"[crawl] Linux candidates: {len(linux)}; MSI candidates: {len(msi)}")
if dry_run:
log("[crawl] Dry-run mode: not downloading. Set CRYPTOPRO_DRY_RUN=0 and CRYPTOPRO_OUTPUT to enable download.")
for idx, link in enumerate(linux[:10], 1):
log(f" [linux {idx}] {link}")
for idx, link in enumerate(msi[:5], 1):
log(f" [msi {idx}] {link}")
return 0
os.makedirs(os.path.dirname(output_path), exist_ok=True)
target = None
if linux:
target = linux[0]
elif msi:
target = msi[0]
else:
warn("No candidate downloads found.")
return 1
log(f"[download] Fetching {target} -> {output_path}")
size = download(target, output_path, auth_handler)
log(f"[download] Complete, size={size} bytes")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -428,6 +428,13 @@ test_hash_performance() {
[[ $duration -lt 10000 ]] || return 1
}
# CryptoPro downloader dry-run (Playwright)
test_downloader_dry_run() {
docker exec "${CONTAINER_NAME}" \
env CRYPTOPRO_DRY_RUN=1 CRYPTOPRO_UNPACK=0 CRYPTOPRO_FETCH_ON_START=1 \
/usr/local/bin/download-cryptopro.sh
}
# ==============================================================================
# Test Runner
# ==============================================================================
@@ -438,6 +445,13 @@ run_all_tests() {
log "Target: ${WINE_CSP_URL}"
log ""
# Downloader dry-run (only when we control the container)
if [[ "${CLEANUP_CONTAINER}" == "true" ]]; then
run_test "cryptopro_downloader_dry_run" test_downloader_dry_run
else
record_test "cryptopro_downloader_dry_run" "skip" "0" "External endpoint; downloader test skipped"
fi
# Health tests
log "--- Health Endpoints ---"
run_test "health_endpoint" test_health_endpoint