#!/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); });