up
This commit is contained in:
185
ops/cryptopro/install-linux-csp.sh
Normal file
185
ops/cryptopro/install-linux-csp.sh
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/bin/bash
|
||||
# CryptoPro CSP 5.0 R3 Linux installer (deb packages)
|
||||
# Uses locally provided .deb packages under /opt/cryptopro/downloads (host volume).
|
||||
# No Wine dependency. Runs offline against the supplied packages only.
|
||||
#
|
||||
# Env:
|
||||
# CRYPTOPRO_INSTALL_FROM Path to folder with .deb packages (default /opt/cryptopro/downloads)
|
||||
# CRYPTOPRO_ACCEPT_EULA Must be 1 to proceed (default 0 -> hard stop with warning)
|
||||
# CRYPTOPRO_SKIP_APT_FIX Set to 1 to skip `apt-get -f install` (offline strict)
|
||||
# CRYPTOPRO_PACKAGE_FILTER Optional glob (e.g., "cprocsp*amd64.deb") to narrow selection
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success; 1 missing dir/files; 2 incompatible arch; 3 EULA not accepted.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_FROM="${CRYPTOPRO_INSTALL_FROM:-/opt/cryptopro/downloads}"
|
||||
PACKAGE_FILTER="${CRYPTOPRO_PACKAGE_FILTER:-*.deb}"
|
||||
SKIP_APT_FIX="${CRYPTOPRO_SKIP_APT_FIX:-0}"
|
||||
STAGING_DIR="/tmp/cryptopro-debs"
|
||||
MINIMAL="${CRYPTOPRO_MINIMAL:-1}"
|
||||
INCLUDE_PLUGIN="${CRYPTOPRO_INCLUDE_PLUGIN:-0}"
|
||||
|
||||
arch_from_uname() {
|
||||
local raw
|
||||
raw="$(uname -m)"
|
||||
case "${raw}" in
|
||||
x86_64) echo "amd64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
arm64) echo "arm64" ;;
|
||||
i386|i686) echo "i386" ;;
|
||||
*) echo "${raw}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
HOST_ARCH="$(dpkg --print-architecture 2>/dev/null || arch_from_uname)"
|
||||
|
||||
log() {
|
||||
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [cryptopro-install] $*"
|
||||
}
|
||||
|
||||
log_err() {
|
||||
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [cryptopro-install] [ERROR] $*" >&2
|
||||
}
|
||||
|
||||
require_eula() {
|
||||
if [[ "${CRYPTOPRO_ACCEPT_EULA:-0}" != "1" ]]; then
|
||||
log_err "License not accepted. Set CRYPTOPRO_ACCEPT_EULA=1 only if you hold a valid CryptoPro license for these binaries and agree to the vendor EULA."
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_extract_bundle() {
|
||||
# Prefer a bundle that matches host arch in filename, otherwise first *.tgz
|
||||
mapfile -t TGZ < <(find "${INSTALL_FROM}" -maxdepth 1 -type f -name "*.tgz" -print 2>/dev/null | sort)
|
||||
if [[ ${#TGZ[@]} -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
local chosen=""
|
||||
for candidate in "${TGZ[@]}"; do
|
||||
if [[ "${candidate}" == *"${HOST_ARCH}"* ]]; then
|
||||
chosen="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "${chosen}" ]]; then
|
||||
chosen="${TGZ[0]}"
|
||||
fi
|
||||
log "Extracting bundle ${chosen} into ${STAGING_DIR}"
|
||||
rm -rf "${STAGING_DIR}"
|
||||
mkdir -p "${STAGING_DIR}"
|
||||
tar -xf "${chosen}" -C "${STAGING_DIR}"
|
||||
# If bundle contains a single subfolder, use it as install root
|
||||
local subdir
|
||||
subdir="$(find "${STAGING_DIR}" -maxdepth 1 -type d ! -path "${STAGING_DIR}" | head -n1)"
|
||||
if [[ -n "${subdir}" ]]; then
|
||||
INSTALL_FROM="${subdir}"
|
||||
else
|
||||
INSTALL_FROM="${STAGING_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
gather_packages() {
|
||||
if [[ ! -d "${INSTALL_FROM}" ]]; then
|
||||
log_err "Package directory not found: ${INSTALL_FROM}"
|
||||
exit 1
|
||||
fi
|
||||
maybe_extract_bundle
|
||||
mapfile -t PKGS < <(find "${INSTALL_FROM}" -maxdepth 2 -type f -name "${PACKAGE_FILTER}" -print 2>/dev/null | sort)
|
||||
if [[ ${#PKGS[@]} -eq 0 ]]; then
|
||||
log_err "No .deb packages found in ${INSTALL_FROM} (filter=${PACKAGE_FILTER})"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
apply_minimal_filter() {
|
||||
if [[ "${MINIMAL}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
local -a keep_exact=(
|
||||
"lsb-cprocsp-base"
|
||||
"lsb-cprocsp-ca-certs"
|
||||
"lsb-cprocsp-capilite-64"
|
||||
"lsb-cprocsp-kc1-64"
|
||||
"lsb-cprocsp-pkcs11-64"
|
||||
"lsb-cprocsp-rdr-64"
|
||||
"cprocsp-curl-64"
|
||||
"cprocsp-pki-cades-64"
|
||||
"cprocsp-compat-debian"
|
||||
)
|
||||
if [[ "${INCLUDE_PLUGIN}" == "1" ]]; then
|
||||
keep_exact+=("cprocsp-pki-plugin-64" "cprocsp-rdr-gui-gtk-64")
|
||||
fi
|
||||
local -a filtered=()
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
local name
|
||||
name="$(dpkg-deb -f "${pkg}" Package 2>/dev/null || basename "${pkg}")"
|
||||
for wanted in "${keep_exact[@]}"; do
|
||||
if [[ "${name}" == "${wanted}" ]]; then
|
||||
filtered+=("${pkg}")
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ ${#filtered[@]} -gt 0 ]]; then
|
||||
log "Applying minimal package set (CRYPTOPRO_MINIMAL=1); kept ${#filtered[@]} of ${#PKGS[@]}"
|
||||
PKGS=("${filtered[@]}")
|
||||
else
|
||||
log "Minimal filter yielded no matches; using full package set"
|
||||
fi
|
||||
}
|
||||
|
||||
filter_by_arch() {
|
||||
FILTERED=()
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
local pkg_arch
|
||||
pkg_arch="$(dpkg-deb -f "${pkg}" Architecture 2>/dev/null || echo "unknown")"
|
||||
if [[ "${pkg_arch}" == "all" || "${pkg_arch}" == "${HOST_ARCH}" ]]; then
|
||||
FILTERED+=("${pkg}")
|
||||
else
|
||||
log "Skipping ${pkg} (arch=${pkg_arch}, host=${HOST_ARCH})"
|
||||
fi
|
||||
done
|
||||
if [[ ${#FILTERED[@]} -eq 0 ]]; then
|
||||
log_err "No packages match host architecture ${HOST_ARCH}"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
print_matrix() {
|
||||
log "Discovered packages (arch filter: host=${HOST_ARCH}):"
|
||||
for pkg in "${FILTERED[@]}"; do
|
||||
local name ver arch
|
||||
name="$(dpkg-deb -f "${pkg}" Package 2>/dev/null || basename "${pkg}")"
|
||||
ver="$(dpkg-deb -f "${pkg}" Version 2>/dev/null || echo "unknown")"
|
||||
arch="$(dpkg-deb -f "${pkg}" Architecture 2>/dev/null || echo "unknown")"
|
||||
echo " - ${name} ${ver} (${arch}) <- ${pkg}"
|
||||
done
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
log "Installing ${#FILTERED[@]} package(s) from ${INSTALL_FROM}"
|
||||
if ! dpkg -i "${FILTERED[@]}"; then
|
||||
if [[ "${SKIP_APT_FIX}" == "1" ]]; then
|
||||
log_err "dpkg reported errors and CRYPTOPRO_SKIP_APT_FIX=1; aborting."
|
||||
exit 1
|
||||
fi
|
||||
log "Resolving dependencies with apt-get -f install (may require network if deps missing locally)"
|
||||
apt-get update >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -f install
|
||||
fi
|
||||
log "CryptoPro packages installed. Verify with: dpkg -l | grep cprocsp"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_eula
|
||||
gather_packages
|
||||
apply_minimal_filter
|
||||
filter_by_arch
|
||||
print_matrix
|
||||
install_packages
|
||||
log "Installation finished. For headless/server use on Ubuntu 22.04 (amd64), the 'linux-amd64_deb.tgz' bundle is preferred and auto-selected."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
31
ops/cryptopro/linux-csp-service/Dockerfile
Normal file
31
ops/cryptopro/linux-csp-service/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
CRYPTOPRO_ACCEPT_EULA=1 \
|
||||
CRYPTOPRO_MINIMAL=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System deps
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip tar xz-utils && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy CryptoPro packages (provided in repo) and installer
|
||||
COPY opt/cryptopro/downloads/*.tgz /opt/cryptopro/downloads/
|
||||
COPY ops/cryptopro/install-linux-csp.sh /usr/local/bin/install-linux-csp.sh
|
||||
RUN chmod +x /usr/local/bin/install-linux-csp.sh
|
||||
|
||||
# Install CryptoPro CSP
|
||||
RUN /usr/local/bin/install-linux-csp.sh
|
||||
|
||||
# Python deps
|
||||
COPY ops/cryptopro/linux-csp-service/requirements.txt /app/requirements.txt
|
||||
RUN pip3 install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
# App
|
||||
COPY ops/cryptopro/linux-csp-service/app.py /app/app.py
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
25
ops/cryptopro/linux-csp-service/README.md
Normal file
25
ops/cryptopro/linux-csp-service/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# CryptoPro Linux CSP Service (experimental)
|
||||
|
||||
Minimal FastAPI wrapper around the Linux CryptoPro CSP binaries to prove installation and expose simple operations.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
docker build -t cryptopro-linux-csp -f ops/cryptopro/linux-csp-service/Dockerfile .
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:8080 cryptopro-linux-csp
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
- `GET /health` — checks `csptest` presence.
|
||||
- `GET /license` — runs `csptest -license`.
|
||||
- `POST /hash` with `{ "data_b64": "<base64>" }` — runs `csptest -hash -hash_alg gost12_256`.
|
||||
|
||||
## Notes
|
||||
- Uses the provided CryptoPro `.tgz` bundles under `opt/cryptopro/downloads`. Ensure you have rights to these binaries; the image builds with `CRYPTOPRO_ACCEPT_EULA=1`.
|
||||
- Default install is minimal (no browser/plugin). Set `CRYPTOPRO_INCLUDE_PLUGIN=1` if you need plugin packages.
|
||||
- This is not a production service; intended for validation only.
|
||||
57
ops/cryptopro/linux-csp-service/app.py
Normal file
57
ops/cryptopro/linux-csp-service/app.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import base64
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="CryptoPro Linux CSP Service", version="0.1.0")
|
||||
|
||||
CSPTEST = Path("/opt/cprocsp/bin/amd64/csptest")
|
||||
|
||||
|
||||
def run_cmd(cmd: list[str], input_bytes: Optional[bytes] = None, allow_fail: bool = False) -> str:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
input=input_bytes,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
check=True,
|
||||
)
|
||||
return proc.stdout.decode("utf-8", errors="replace")
|
||||
except subprocess.CalledProcessError as exc:
|
||||
output = exc.stdout.decode("utf-8", errors="replace") if exc.stdout else ""
|
||||
if allow_fail:
|
||||
return output
|
||||
raise HTTPException(status_code=500, detail={"cmd": cmd, "output": output})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
if not CSPTEST.exists():
|
||||
raise HTTPException(status_code=500, detail="csptest binary not found; ensure CryptoPro CSP is installed")
|
||||
return {"status": "ok", "csptest": str(CSPTEST)}
|
||||
|
||||
|
||||
@app.get("/license")
|
||||
def license_info():
|
||||
output = run_cmd([str(CSPTEST), "-keyset", "-info"], allow_fail=True)
|
||||
return {"output": output}
|
||||
|
||||
|
||||
class HashRequest(BaseModel):
|
||||
data_b64: str
|
||||
|
||||
|
||||
@app.post("/hash")
|
||||
def hash_data(body: HashRequest):
|
||||
try:
|
||||
data = base64.b64decode(body.data_b64)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid base64")
|
||||
|
||||
cmd = [str(CSPTEST), "-hash", "-in", "-", "-hash_alg", "gost12_256"]
|
||||
output = run_cmd(cmd, input_bytes=data)
|
||||
return {"output": output}
|
||||
2
ops/cryptopro/linux-csp-service/requirements.txt
Normal file
2
ops/cryptopro/linux-csp-service/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.1
|
||||
@@ -54,7 +54,7 @@ SCANNER_SURFACE_SECRETS_ALLOW_INLINE=false
|
||||
ZASTAVA_SURFACE_SECRETS_PROVIDER=${SCANNER_SURFACE_SECRETS_PROVIDER}
|
||||
ZASTAVA_SURFACE_SECRETS_ROOT=${SCANNER_SURFACE_SECRETS_ROOT}
|
||||
```
|
||||
4) Ensure docker-compose mounts the secrets path read-only to the services that need it.
|
||||
4) Ensure docker-compose mounts the secrets path read-only to the services that need it. Use `SURFACE_SECRETS_HOST_PATH` to point at the decrypted bundle on the host (defaults to `./offline/surface-secrets` in the Compose profiles).
|
||||
|
||||
## Offline Kit workflow
|
||||
- The offline kit already ships encrypted `surface-secrets` bundles (see `docs/24_OFFLINE_KIT.md`).
|
||||
|
||||
12
ops/sm-remote/Dockerfile
Normal file
12
ops/sm-remote/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# Simulated SM2 remote microservice (software-only)
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet publish src/SmRemote/StellaOps.SmRemote.Service/StellaOps.SmRemote.Service.csproj -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:56080
|
||||
ENV SM_SOFT_ALLOWED=1
|
||||
ENTRYPOINT ["dotnet", "StellaOps.SmRemote.Service.dll"]
|
||||
@@ -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}
|
||||
|
||||
62
ops/wine-csp/download-cryptopro.sh
Normal file
62
ops/wine-csp/download-cryptopro.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
164
ops/wine-csp/fetch-cryptopro.py
Normal file
164
ops/wine-csp/fetch-cryptopro.py
Normal 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())
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user