221 lines
8.1 KiB
JavaScript
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);
|
|
});
|