diff --git a/lib/LocalBinary.js b/lib/LocalBinary.js index 4f322b5..8d694b2 100644 --- a/lib/LocalBinary.js +++ b/lib/LocalBinary.js @@ -1,14 +1,15 @@ var https = require('https'), - url = require('url'), fs = require('fs'), path = require('path'), os = require('os'), + url = require('url'), util = require('util'), childProcess = require('child_process'), zlib = require('zlib'), HttpsProxyAgent = require('https-proxy-agent'), version = require('../package.json').version, - LocalError = require('./LocalError'); + LocalError = require('./LocalError'), + fetchDownloadSourceUrlAsync = require('./fetchDownloadSourceUrlAsync'); const packageName = 'browserstack-local-nodejs'; @@ -20,7 +21,7 @@ function LocalBinary(){ this.sourceURL = null; this.downloadErrorMessage = null; - this.getSourceUrl = function(conf, retries) { + this.getSourceUrlSync = function(conf, retries) { /* Request for an endpoint to download the local binary from Rails no more than twice with 5 retries each */ if (![4, 9].includes(retries) && this.sourceURL != null) { return this.sourceURL; @@ -63,28 +64,60 @@ function LocalBinary(){ } }; - this.getDownloadPath = function (conf, retries) { - let sourceURL = this.getSourceUrl(conf, retries) + '/'; + this.getSourceUrl = function(conf, retries, callback) { + /* Request for an endpoint to download the local binary from Rails no more than twice with 5 retries each */ + if (![4, 9].includes(retries) && this.sourceURL != null) { + return callback(null, this.sourceURL); + } + + if (process.env.BINARY_DOWNLOAD_SOURCE_URL !== undefined && process.env.BINARY_DOWNLOAD_FALLBACK_ENABLED == 'true' && this.parentRetries != 4) { + /* This is triggered from Local.js if there's an error executing the downloaded binary */ + return callback(null, process.env.BINARY_DOWNLOAD_SOURCE_URL); + } + + let downloadFallback = false; + let downloadErrorMessage = null; + + if (retries == 4 || (process.env.BINARY_DOWNLOAD_FALLBACK_ENABLED == 'true' && this.parentRetries == 4)) { + downloadFallback = true; + downloadErrorMessage = this.downloadErrorMessage || process.env.BINARY_DOWNLOAD_ERROR_MESSAGE; + } + + fetchDownloadSourceUrlAsync(this.key, this.bsHost, downloadFallback, downloadErrorMessage, conf.proxyHost, conf.proxyPort, conf.useCaCertificate, (err, sourceURL) => { + if (err) return callback(err); + this.sourceURL = sourceURL; + process.env.BINARY_DOWNLOAD_SOURCE_URL = sourceURL; + callback(null, sourceURL); + }); + }; + this.getBinaryFilename = function() { if(this.hostOS.match(/darwin|mac os/i)){ - return sourceURL + 'BrowserStackLocal-darwin-x64'; + return 'BrowserStackLocal-darwin-x64'; } else if(this.hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) { this.windows = true; - return sourceURL + 'BrowserStackLocal.exe'; + return 'BrowserStackLocal.exe'; } else { if(this.isArm64) { - return sourceURL + 'BrowserStackLocal-linux-arm64'; + return 'BrowserStackLocal-linux-arm64'; } else if(this.is64bits) { if(this.isAlpine()) - return sourceURL + 'BrowserStackLocal-alpine'; + return 'BrowserStackLocal-alpine'; else - return sourceURL + 'BrowserStackLocal-linux-x64'; + return 'BrowserStackLocal-linux-x64'; } else { - return sourceURL + 'BrowserStackLocal-linux-ia32'; + return 'BrowserStackLocal-linux-ia32'; } } }; + this.getDownloadPath = function (conf, retries, callback) { + this.getSourceUrl(conf, retries, (err, sourceURL) => { + if (err) return callback(err); + callback(null, sourceURL + '/' + this.getBinaryFilename()); + }); + }; + this.isAlpine = function() { try { return childProcess.execSync('grep -w "NAME" /etc/os-release').includes('Alpine'); @@ -118,7 +151,7 @@ function LocalBinary(){ this.downloadSync = function(conf, destParentDir, retries) { try { - this.httpPath = this.getDownloadPath(conf, retries); + this.httpPath = this.getSourceUrlSync(conf, retries) + '/' + this.getBinaryFilename(); } catch (e) { return console.error(`Unable to fetch the source url to download the binary with error: ${e}`); } @@ -168,68 +201,70 @@ function LocalBinary(){ }; this.download = function(conf, destParentDir, callback, retries){ - try { - this.httpPath = this.getDownloadPath(conf, retries); - } catch (e) { - return console.error(`Unable to fetch the source url to download the binary with error: ${e}`); - } - - var that = this; - if(!this.checkPath(destParentDir)) - fs.mkdirSync(destParentDir); + this.getDownloadPath(conf, retries, (err, downloadUrl) => { + if(err) { + return console.error('Unable to fetch the source url to download the binary with error: ', err); + } - var destBinaryName = (this.windows) ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'; - var binaryPath = path.join(destParentDir, destBinaryName); - var fileStream = fs.createWriteStream(binaryPath); + this.httpPath = downloadUrl; - var options = url.parse(this.httpPath); - if(conf.proxyHost && conf.proxyPort) { - options.agent = new HttpsProxyAgent({ - host: conf.proxyHost, - port: conf.proxyPort - }); - } - if (conf.useCaCertificate) { - try { - options.ca = fs.readFileSync(conf.useCaCertificate); - } catch(err) { - console.log('failed to read cert file', err); - } - } + var that = this; + if(!this.checkPath(destParentDir)) + fs.mkdirSync(destParentDir); - options.headers = Object.assign({}, options.headers, { - 'accept-encoding': 'gzip, *', - 'user-agent': [packageName, version].join('/'), - }); + var destBinaryName = (this.windows) ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'; + var binaryPath = path.join(destParentDir, destBinaryName); + var fileStream = fs.createWriteStream(binaryPath); - https.get(options, function (response) { - const contentEncoding = response.headers['content-encoding']; - if (typeof contentEncoding === 'string' && contentEncoding.match(/gzip/i)) { - if (process.env.BROWSERSTACK_LOCAL_DEBUG_GZIP) { - console.info('Using gzip in ' + options.headers['user-agent']); + var options = url.parse(this.httpPath); + if(conf.proxyHost && conf.proxyPort) { + options.agent = new HttpsProxyAgent({ + host: conf.proxyHost, + port: conf.proxyPort + }); + } + if (conf.useCaCertificate) { + try { + options.ca = fs.readFileSync(conf.useCaCertificate); + } catch(err) { + console.log('failed to read cert file', err); } - - response.pipe(zlib.createGunzip()).pipe(fileStream); - } else { - response.pipe(fileStream); } - response.on('error', function(err) { - that.binaryDownloadError('Got Error in binary download response', util.format(err)); - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); + options.headers = Object.assign({}, options.headers, { + 'accept-encoding': 'gzip, *', + 'user-agent': [packageName, version].join('/'), }); - fileStream.on('error', function (err) { - that.binaryDownloadError('Got Error while downloading binary file', util.format(err)); - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); - }); - fileStream.on('close', function () { - fs.chmod(binaryPath, '0755', function() { - callback(binaryPath); + + https.get(options, function (response) { + const contentEncoding = response.headers['content-encoding']; + if (typeof contentEncoding === 'string' && contentEncoding.match(/gzip/i)) { + if (process.env.BROWSERSTACK_LOCAL_DEBUG_GZIP) { + console.info('Using gzip in ' + options.headers['user-agent']); + } + + response.pipe(zlib.createGunzip()).pipe(fileStream); + } else { + response.pipe(fileStream); + } + + response.on('error', function(err) { + that.binaryDownloadError('Got Error in binary download response', util.format(err)); + that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); }); + fileStream.on('error', function (err) { + that.binaryDownloadError('Got Error while downloading binary file', util.format(err)); + that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); + }); + fileStream.on('close', function () { + fs.chmod(binaryPath, '0755', function() { + callback(binaryPath); + }); + }); + }).on('error', function(err) { + that.binaryDownloadError('Got Error in binary downloading request', util.format(err)); + that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); }); - }).on('error', function(err) { - that.binaryDownloadError('Got Error in binary downloading request', util.format(err)); - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); }); }; diff --git a/lib/fetchDownloadSourceUrlAsync.js b/lib/fetchDownloadSourceUrlAsync.js new file mode 100644 index 0000000..ad6e97a --- /dev/null +++ b/lib/fetchDownloadSourceUrlAsync.js @@ -0,0 +1,70 @@ +const https = require('https'), + fs = require('fs'), + HttpsProxyAgent = require('https-proxy-agent'), + { isUndefined } = require('./util'), + version = require('../package.json').version; + +const packageName = 'browserstack-local-nodejs'; + +function fetchDownloadSourceUrlAsync(authToken, bsHost, downloadFallback, downloadErrorMessage, proxyHost, proxyPort, useCaCertificate, callback) { + let body = '', data = {'auth_token': authToken}; + const userAgent = [packageName, version].join('/'); + const options = { + hostname: !isUndefined(bsHost) ? bsHost : 'local.browserstack.com', + port: 443, + path: '/binary/api/v1/endpoint', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'user-agent': userAgent + } + }; + if (downloadFallback == 'true') { + options.headers['X-Local-Fallback-Cloudflare'] = true; + data['error_message'] = downloadErrorMessage; + } + + if(!isUndefined(proxyHost) && !isUndefined(proxyPort)) { + options.agent = new HttpsProxyAgent({ + host: proxyHost, + port: proxyPort + }); + } + if (!isUndefined(useCaCertificate)) { + try { + options.ca = fs.readFileSync(useCaCertificate); + } catch(err) { + console.log('failed to read cert file', err); + } + } + + const req = https.request(options, res => { + res.on('data', d => { + body += d; + }); + res.on('end', () => { + try { + const reqBody = JSON.parse(body); + if(reqBody.error) { + throw reqBody.error; + } + callback(null, reqBody.data.endpoint); + } catch (e) { + console.error(e); + callback(e); + } + }); + res.on('error', (err) => { + console.error(err); + callback(err); + }); + }); + req.on('error', e => { + console.error(e); + callback(e); + }); + req.write(JSON.stringify(data)); + req.end(); +} + +module.exports = fetchDownloadSourceUrlAsync;