download.js 5.74 KB
/*
- downloads archived binary from ngrok cdn (if not found in local cache)
- stores it in home dir as a local cache
- extracts executable to module's bin folder
*/

function downloadNgrok(callback, options) {
  options = options || {};

  const os = require('os');
  const fs = require('fs');
  const path = require('path');
  const readline = require('readline');
  const Zip = require('decompress-zip');
  const request = require('request');
  const { Transform } = require('stream');

  const cafilePath = options.cafilePath || process.env.NGROK_ROOT_CA_PATH;
  const cdnUrl = getCdnUrl();
  const cacheUrl = getCacheUrl();
  const maxAttempts = 3;
  let attempts = 0;

  if (hasCache()) {
    console.log('ngrok - cached download found at ' + cacheUrl);
    extract(retry);
  } else {
    download(retry);
  }

  function getCdnUrl() {
    const arch = options.arch || process.env.NGROK_ARCH || os.platform() + os.arch();
    const cdn = options.cdnUrl || process.env.NGROK_CDN_URL || 'https://bin.equinox.io';
    const cdnPath = options.cdnPath || process.env.NGROK_CDN_PATH || '/c/4VmDzA7iaHb/ngrok-stable-';
    const cdnFiles = {
      darwinia32: cdn + cdnPath + 'darwin-386.zip',
      darwinx64: cdn + cdnPath + 'darwin-amd64.zip',
      linuxarm: cdn + cdnPath + 'linux-arm.zip',
      linuxarm64: cdn + cdnPath + 'linux-arm64.zip',
      androidarm: cdn + cdnPath + 'linux-arm.zip',
      androidarm64: cdn + cdnPath + 'linux-arm64.zip',
      linuxia32: cdn + cdnPath + 'linux-386.zip',
      linuxx64: cdn + cdnPath + 'linux-amd64.zip',
      win32ia32: cdn + cdnPath + 'windows-386.zip',
      win32x64: cdn + cdnPath + 'windows-amd64.zip',
      freebsdia32: cdn + cdnPath + 'freebsd-386.zip',
      freebsdx64: cdn + cdnPath + 'freebsd-amd64.zip'
    };
    const url = cdnFiles[arch];
    if (!url) {
      console.error('ngrok - platform ' + arch + ' is not supported.');
      process.exit(1);
    }
    return url;
  }

  function getCacheUrl() {
    let dir;
    try {
      dir = path.join(os.homedir(), '.ngrok');
      if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
        fs.mkdirSync(dir);
      }
    } catch (err) {
      dir = path.join(__dirname, 'bin');
    }
    const name = Buffer.from(cdnUrl).toString('base64');
    return path.join(dir, name + '.zip');
  }

  function hasCache() {
    if (options.ignoreCache || process.env.NGROK_IGNORE_CACHE === 'true') {
      return false;
    }
    return fs.existsSync(cacheUrl) && fs.statSync(cacheUrl).size;
  }

  function download(cb) {
    console.log('ngrok - downloading binary ' + cdnUrl);

    const ca = tryToReadCaFile();

    const options = {
      url: cdnUrl,
      ca
    };

    const downloadStream = request
      .get(options)
      .on('response', res => {
        if (!/2\d\d/.test(res.statusCode)) {
          res.pause();
          return downloadStream.emit('error', new Error('wrong status code: ' + res.statusCode));
        }
        const total = res.headers['content-length'];
        const progress = progressStream('ngrok - downloading progress: ', total);
        res.pipe(progress).pipe(outputStream);
      })
      .on('error', e => {
        console.warn('ngrok - error downloading binary', e);
        cb(e);
      });

    const outputStream = fs
      .createWriteStream(cacheUrl)
      .on('error', e => {
        console.log('ngrok - error storing binary to local file', e);
        cb(e);
      })
      .on('finish', () => {
        console.log('\nngrok - binary downloaded to ' + cacheUrl);
        extract(cb);
      });
  }

  function progressStream(msg, total) {
    let downloaded = 0;
    let shouldClearLine = false;
    const log = () => {
      if (shouldClearLine) {
        readline.clearLine(process.stdout);
        readline.cursorTo(process.stdout, 0);
      }
      let progress = downloaded + (total ? '/' + total : '');
      process.stdout.write(msg + progress);
      shouldClearLine = true;
    };
    if (total > 0) log();
    return new Transform({
      transform(data, enc, cb) {
        downloaded += data.length;
        log();
        cb(null, data);
      }
    });
  }

  function extract(cb) {
    console.log('ngrok - unpacking binary');
    const moduleBinPath = path.join(__dirname, 'bin');
    new Zip(cacheUrl)
      .extract({ path: moduleBinPath })
      .once('error', error)
      .once('extract', () => {
        const suffix = os.platform() === 'win32' ? '.exe' : '';
        if (suffix === '.exe') {
          fs.writeFileSync(path.join(moduleBinPath, 'ngrok.cmd'), 'ngrok.exe');
        }
        const target = path.join(moduleBinPath, 'ngrok' + suffix);
        fs.chmodSync(target, 0755);
        if (!fs.existsSync(target) || fs.statSync(target).size <= 0) {
          return error(new Error('corrupted file ' + target));
        }
        console.log('ngrok - binary unpacked to ' + target);
        cb(null);
      });

    function error(e) {
      console.warn('ngrok - error unpacking binary', e);
      cb(e);
    }
  }

  function retry(err) {
    attempts++;
    if (err && attempts === maxAttempts) {
      console.error('ngrok - install failed', err);
      return callback(err);
    }
    if (err) {
      console.warn('ngrok - install failed, retrying');
      return setTimeout(download, 500, retry);
    }
    callback();
  }

  function tryToReadCaFile() {
    try {
      const caString = fs.existsSync(cafilePath) && fs.readFileSync(cafilePath).toString();

      const caContents =
        caString &&
        caString
          .split('-----END CERTIFICATE-----')
          .filter(c => c.trim().startsWith('-----BEGIN CERTIFICATE-----'))
          .map(c => `${c}-----END CERTIFICATE-----`);

      return caContents.length > 0 ? caContents : undefined;
    } catch (error) {
      console.warn(error);
      return undefined;
    }
  }
}

module.exports = downloadNgrok;