Files
git.stella-ops.org/scripts/crypto/download-cryptopro-playwright.cjs
StellaOps Bot bc0762e97d up
2025-12-09 00:20:52 +02:00

221 lines
8.1 KiB
JavaScript

#!/usr/bin/env node
/**
* CryptoPro CSP downloader (Playwright-driven).
*
* Navigates cryptopro.ru downloads page, optionally fills login form, and selects
* Linux packages (.rpm/.deb/.tar.gz/.tgz/.bin) under the CSP Linux section.
*
* Environment:
* - CRYPTOPRO_URL (default: https://cryptopro.ru/products/csp/downloads#latest_csp50r3_linux)
* - CRYPTOPRO_EMAIL / CRYPTOPRO_PASSWORD (default demo creds: contact@stella-ops.org / Hoko33JD3nj3aJD.)
* - CRYPTOPRO_DRY_RUN (default: 1) -> list candidates, do not download
* - CRYPTOPRO_OUTPUT_DIR (default: /opt/cryptopro/downloads)
* - CRYPTOPRO_OUTPUT_FILE (optional: force a specific output filename/path)
* - CRYPTOPRO_UNPACK (default: 0) -> attempt to unpack tar.gz/tgz/rpm/deb
*/
const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');
const { chromium } = require('playwright-chromium');
const url = process.env.CRYPTOPRO_URL || 'https://cryptopro.ru/products/csp/downloads#latest_csp50r3_linux';
const email = process.env.CRYPTOPRO_EMAIL || 'contact@stella-ops.org';
const password = process.env.CRYPTOPRO_PASSWORD || 'Hoko33JD3nj3aJD.';
const dryRun = (process.env.CRYPTOPRO_DRY_RUN || '1') !== '0';
const outputDir = process.env.CRYPTOPRO_OUTPUT_DIR || '/opt/cryptopro/downloads';
const outputFile = process.env.CRYPTOPRO_OUTPUT_FILE;
const unpack = (process.env.CRYPTOPRO_UNPACK || '0') === '1';
const navTimeout = parseInt(process.env.CRYPTOPRO_NAV_TIMEOUT || '60000', 10);
const linuxPattern = /\.(rpm|deb|tar\.gz|tgz|bin)(\?|$)/i;
const debugLinks = (process.env.CRYPTOPRO_DEBUG || '0') === '1';
function log(msg) {
process.stdout.write(`${msg}\n`);
}
function warn(msg) {
process.stderr.write(`[WARN] ${msg}\n`);
}
async function maybeLogin(page) {
const emailSelector = 'input[type="email"], input[name*="email" i], input[name*="login" i], input[name="name"]';
const passwordSelector = 'input[type="password"], input[name*="password" i]';
const submitSelector = 'button[type="submit"], input[type="submit"]';
const emailInput = await page.$(emailSelector);
const passwordInput = await page.$(passwordSelector);
if (emailInput && passwordInput) {
log('[login] Form detected; submitting credentials');
await emailInput.fill(email);
await passwordInput.fill(password);
const submit = await page.$(submitSelector);
if (submit) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}),
submit.click()
]);
} else {
await passwordInput.press('Enter');
await page.waitForTimeout(2000);
}
} else {
log('[login] No login form detected; continuing anonymously');
}
}
async function findLinuxLinks(page) {
const targets = [page, ...page.frames()];
const hrefs = [];
// Collect href/data-href/data-url across main page + frames
for (const target of targets) {
try {
const collected = await target.$$eval('a[href], [data-href], [data-url]', (els) =>
els
.map((el) => el.getAttribute('href') || el.getAttribute('data-href') || el.getAttribute('data-url'))
.filter((href) => typeof href === 'string')
);
hrefs.push(...collected);
} catch (err) {
warn(`[scan] Failed to collect links from frame: ${err.message}`);
}
}
const unique = Array.from(new Set(hrefs));
return unique.filter((href) => linuxPattern.test(href));
}
function unpackIfSupported(filePath) {
if (!unpack) {
return;
}
const cwd = path.dirname(filePath);
if (filePath.endsWith('.tar.gz') || filePath.endsWith('.tgz')) {
const res = spawnSync('tar', ['-xzf', filePath, '-C', cwd], { stdio: 'inherit' });
if (res.status === 0) {
log(`[unpack] Extracted ${filePath}`);
} else {
warn(`[unpack] Failed to extract ${filePath}`);
}
} else if (filePath.endsWith('.rpm')) {
const res = spawnSync('bash', ['-lc', `rpm2cpio "${filePath}" | cpio -idmv`], { stdio: 'inherit', cwd });
if (res.status === 0) {
log(`[unpack] Extracted RPM ${filePath}`);
} else {
warn(`[unpack] Failed to extract RPM ${filePath}`);
}
} else if (filePath.endsWith('.deb')) {
const res = spawnSync('dpkg-deb', ['-x', filePath, cwd], { stdio: 'inherit' });
if (res.status === 0) {
log(`[unpack] Extracted DEB ${filePath}`);
} else {
warn(`[unpack] Failed to extract DEB ${filePath}`);
}
} else if (filePath.endsWith('.bin')) {
const res = spawnSync('chmod', ['+x', filePath], { stdio: 'inherit' });
if (res.status === 0) {
log(`[unpack] Marked ${filePath} as executable (self-extract expected)`);
} else {
warn(`[unpack] Could not mark ${filePath} executable`);
}
} else {
warn(`[unpack] Skipping unsupported archive type for ${filePath}`);
}
}
async function main() {
if (email === 'contact@stella-ops.org' && password === 'Hoko33JD3nj3aJD.') {
warn('Using default demo credentials; set CRYPTOPRO_EMAIL/CRYPTOPRO_PASSWORD to real customer creds.');
}
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
acceptDownloads: true,
httpCredentials: { username: email, password }
});
const page = await context.newPage();
log(`[nav] Opening ${url}`);
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: navTimeout });
} catch (err) {
warn(`[nav] Navigation at networkidle failed (${err.message}); retrying with waitUntil=load`);
await page.goto(url, { waitUntil: 'load', timeout: navTimeout });
}
log(`[nav] Landed on ${page.url()}`);
await maybeLogin(page);
await page.waitForTimeout(2000);
const loginGate =
page.url().includes('/user') ||
(await page.$('form#user-login, form[id*="user-login"], .captcha, #captcha-container'));
if (loginGate) {
warn('[auth] Login/captcha gate detected on downloads page; automated fetch blocked. Provide session/cookies or run headful to solve manually.');
await browser.close();
return 2;
}
let links = await findLinuxLinks(page);
if (links.length === 0) {
await page.waitForTimeout(1500);
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000);
links = await findLinuxLinks(page);
}
if (links.length === 0) {
if (debugLinks) {
const targetDir = outputFile ? path.dirname(outputFile) : outputDir;
await fs.promises.mkdir(targetDir, { recursive: true });
const debugHtml = path.join(targetDir, 'cryptopro-download-page.html');
await fs.promises.writeFile(debugHtml, await page.content(), 'utf8');
log(`[debug] Saved page HTML to ${debugHtml}`);
const allLinks = await page.$$eval('a[href], [data-href], [data-url]', (els) =>
els
.map((el) => el.getAttribute('href') || el.getAttribute('data-href') || el.getAttribute('data-url'))
.filter((href) => typeof href === 'string')
);
log(`[debug] Total link-like attributes: ${allLinks.length}`);
allLinks.slice(0, 20).forEach((href, idx) => log(` [all ${idx + 1}] ${href}`));
}
warn('No Linux download links found on page.');
await browser.close();
return 1;
}
log(`[scan] Found ${links.length} Linux candidate links`);
links.slice(0, 10).forEach((href, idx) => log(` [${idx + 1}] ${href}`));
if (dryRun) {
log('[mode] Dry-run enabled; not downloading. Set CRYPTOPRO_DRY_RUN=0 to fetch.');
await browser.close();
return 0;
}
const target = links[0];
log(`[download] Fetching ${target}`);
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 30000 }),
page.goto(target).catch(() => page.click(`a[href="${target}"]`).catch(() => {}))
]);
const targetDir = outputFile ? path.dirname(outputFile) : outputDir;
await fs.promises.mkdir(targetDir, { recursive: true });
const suggested = download.suggestedFilename();
const outPath = outputFile ? outputFile : path.join(outputDir, suggested);
await download.saveAs(outPath);
log(`[download] Saved to ${outPath}`);
unpackIfSupported(outPath);
await browser.close();
return 0;
}
main()
.then((code) => process.exit(code))
.catch((err) => {
console.error(err);
process.exit(1);
});