123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 'use strict';
- const fs = require('fs');
- const http = require('http');
- const os = require('os');
- const path = require('path');
- const crypto = require('crypto');
- const zlib = require('zlib');
- const stream = require('stream');
- const process = require('process');
- const HttpsProxyAgent = require('https-proxy-agent');
- const fetch = require('node-fetch');
- const ProgressBar = require('progress');
- const Proxy = require('proxy-from-env');
- // NOTE: Can be dropped in favor of `fs.mkdirSync(path, { recursive: true })` once we stop supporting Node 8.x
- const mkdirp = require('mkdirp');
- const which = require('which');
- const helper = require('./helper');
- const pkgInfo = require('../package.json');
- const Logger = require('./logger');
- function getLogStream(defaultStream) {
- const logStream = process.env.SENTRYCLI_LOG_STREAM || defaultStream;
- if (logStream === 'stdout') {
- return process.stdout;
- }
- if (logStream === 'stderr') {
- return process.stderr;
- }
- throw new Error(
- `Incorrect SENTRYCLI_LOG_STREAM env variable. Possible values: 'stdout' | 'stderr'`
- );
- }
- const ttyLogger = new Logger(getLogStream('stderr'));
- const CDN_URL =
- process.env.SENTRYCLI_LOCAL_CDNURL ||
- process.env.npm_config_sentrycli_cdnurl ||
- process.env.SENTRYCLI_CDNURL ||
- 'https://downloads.sentry-cdn.com/sentry-cli';
- function shouldRenderProgressBar() {
- const silentFlag = process.argv.some((v) => v === '--silent');
- const silentConfig = process.env.npm_config_loglevel === 'silent';
- // Leave `SENTRY_NO_PROGRESS_BAR` for backwards compatibility
- const silentEnv = process.env.SENTRYCLI_NO_PROGRESS_BAR || process.env.SENTRY_NO_PROGRESS_BAR;
- const ciEnv = process.env.CI === 'true';
- // If any of possible options is set, skip rendering of progress bar
- return !(silentFlag || silentConfig || silentEnv || ciEnv);
- }
- function getDownloadUrl(platform, arch) {
- const releasesUrl = `${CDN_URL}/${pkgInfo.version}/sentry-cli`;
- let archString = '';
- switch (arch) {
- case 'x64':
- archString = 'x86_64';
- break;
- case 'x86':
- case 'ia32':
- archString = 'i686';
- break;
- case 'arm64':
- archString = 'aarch64';
- break;
- case 'arm':
- archString = 'armv7';
- break;
- default:
- archString = arch;
- }
- switch (platform) {
- case 'darwin':
- return `${releasesUrl}-Darwin-universal`;
- case 'win32':
- return `${releasesUrl}-Windows-${archString}.exe`;
- case 'linux':
- case 'freebsd':
- return `${releasesUrl}-Linux-${archString}`;
- default:
- return null;
- }
- }
- function createProgressBar(name, total) {
- const incorrectTotal = typeof total !== 'number' || Number.isNaN(total);
- if (incorrectTotal || !shouldRenderProgressBar()) {
- return {
- tick: () => {},
- };
- }
- const logStream = getLogStream('stdout');
- if (logStream.isTTY) {
- return new ProgressBar(`fetching ${name} :bar :percent :etas`, {
- complete: '█',
- incomplete: '░',
- width: 20,
- total,
- });
- }
- let pct = null;
- let current = 0;
- return {
- tick: (length) => {
- current += length;
- const next = Math.round((current / total) * 100);
- if (next > pct) {
- pct = next;
- logStream.write(`fetching ${name} ${pct}%\n`);
- }
- },
- };
- }
- function npmCache() {
- const env = process.env;
- return (
- env.npm_config_cache ||
- env.npm_config_cache_folder ||
- env.npm_config_yarn_offline_mirror ||
- (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm'))
- );
- }
- function getCachedPath(url) {
- const digest = crypto.createHash('md5').update(url).digest('hex').slice(0, 6);
- return path.join(
- npmCache(),
- 'sentry-cli',
- `${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, '-')}`
- );
- }
- function getTempFile(cached) {
- return `${cached}.${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
- }
- function validateChecksum(tempPath, name, logger) {
- let storedHash;
- try {
- const checksums = fs.readFileSync(path.join(__dirname, '../checksums.txt'), 'utf8');
- const entries = checksums.split('\n');
- for (let i = 0; i < entries.length; i++) {
- const [key, value] = entries[i].split('=');
- if (key === name) {
- storedHash = value;
- break;
- }
- }
- } catch (e) {
- logger.log(
- 'Checksums are generated when the package is published to npm. They are not available directly in the source repository. Skipping validation.'
- );
- return;
- }
- if (!storedHash) {
- logger.log(`Checksum for ${name} not found, skipping validation.`);
- return;
- }
- const currentHash = crypto.createHash('sha256').update(fs.readFileSync(tempPath)).digest('hex');
- if (storedHash !== currentHash) {
- fs.unlinkSync(tempPath);
- throw new Error(
- `Checksum validation for ${name} failed.\nExpected: ${storedHash}\nReceived: ${currentHash}`
- );
- } else {
- logger.log('Checksum validation passed.');
- }
- }
- function checkVersion() {
- return helper.execute(['--version']).then((output) => {
- const version = output.replace('sentry-cli ', '').trim();
- const expected = process.env.SENTRYCLI_LOCAL_CDNURL ? 'DEV' : pkgInfo.version;
- if (version !== expected) {
- throw new Error(`Unexpected sentry-cli version "${version}", expected "${expected}"`);
- }
- });
- }
- function downloadBinary(logger = ttyLogger) {
- if (process.env.SENTRYCLI_SKIP_DOWNLOAD === '1') {
- logger.log(`Skipping download because SENTRYCLI_SKIP_DOWNLOAD=1 detected.`);
- return Promise.resolve();
- }
- const arch = os.arch();
- const platform = os.platform();
- const outputPath = helper.getPath();
- if (process.env.SENTRYCLI_USE_LOCAL === '1') {
- try {
- const binPath = which.sync('sentry-cli');
- logger.log(`Using local binary: ${binPath}`);
- fs.copyFileSync(binPath, outputPath);
- return Promise.resolve();
- } catch (e) {
- throw new Error(
- 'Configured installation of local binary, but it was not found.' +
- 'Make sure that `sentry-cli` executable is available in your $PATH or disable SENTRYCLI_USE_LOCAL env variable.'
- );
- }
- }
- const downloadUrl = getDownloadUrl(platform, arch);
- if (!downloadUrl) {
- return Promise.reject(new Error(`Unsupported target ${platform}-${arch}`));
- }
- const cachedPath = getCachedPath(downloadUrl);
- if (fs.existsSync(cachedPath)) {
- logger.log(`Using cached binary: ${cachedPath}`);
- fs.copyFileSync(cachedPath, outputPath);
- return Promise.resolve();
- }
- const proxyUrl = Proxy.getProxyForUrl(downloadUrl);
- const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : null;
- logger.log(`Downloading from ${downloadUrl}`);
- if (proxyUrl) {
- logger.log(`Using proxy URL: ${proxyUrl}`);
- }
- return fetch(downloadUrl, {
- agent,
- compress: false,
- headers: {
- 'accept-encoding': 'gzip, deflate, br',
- },
- redirect: 'follow',
- })
- .then((response) => {
- if (!response.ok) {
- throw new Error(
- `Unable to download sentry-cli binary from ${downloadUrl}.\nServer returned ${response.status}: ${response.statusText}.`
- );
- }
- const contentEncoding = response.headers.get('content-encoding');
- let decompressor;
- if (/\bgzip\b/.test(contentEncoding)) {
- decompressor = zlib.createGunzip();
- } else if (/\bdeflate\b/.test(contentEncoding)) {
- decompressor = zlib.createInflate();
- } else if (/\bbr\b/.test(contentEncoding)) {
- decompressor = zlib.createBrotliDecompress();
- } else {
- decompressor = new stream.PassThrough();
- }
- const name = downloadUrl.match(/.*\/(.*?)$/)[1];
- const total = parseInt(response.headers.get('content-length'), 10);
- const progressBar = createProgressBar(name, total);
- const tempPath = getTempFile(cachedPath);
- mkdirp.sync(path.dirname(tempPath));
- return new Promise((resolve, reject) => {
- response.body
- .on('error', (e) => reject(e))
- .on('data', (chunk) => progressBar.tick(chunk.length))
- .pipe(decompressor)
- .pipe(fs.createWriteStream(tempPath, { mode: '0755' }))
- .on('error', (e) => reject(e))
- .on('close', () => resolve());
- }).then(() => {
- if (process.env.SENTRYCLI_SKIP_CHECKSUM_VALIDATION !== '1') {
- validateChecksum(tempPath, name, logger);
- }
- fs.copyFileSync(tempPath, cachedPath);
- fs.copyFileSync(tempPath, outputPath);
- fs.unlinkSync(tempPath);
- });
- })
- .then(() => {
- return checkVersion();
- })
- .catch((error) => {
- if (error instanceof fetch.FetchError) {
- throw new Error(
- `Unable to download sentry-cli binary from ${downloadUrl}.\nError code: ${error.code}`
- );
- } else {
- throw error;
- }
- });
- }
- module.exports = {
- downloadBinary,
- };
|