Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 98 additions & 64 deletions lib/LocalBinary.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
var https = require('https'),
url = require('url'),
fs = require('fs'),
path = require('path'),
os = require('os'),
Expand All @@ -8,7 +7,8 @@ var https = require('https'),
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';

Expand All @@ -20,7 +20,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;
Expand Down Expand Up @@ -63,28 +63,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');
Expand Down Expand Up @@ -118,7 +150,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}`);
}
Expand Down Expand Up @@ -168,68 +200,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, url) => {
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 = url;

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);
});
};

Expand Down
71 changes: 71 additions & 0 deletions lib/fetchDownloadSourceUrlAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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;
}
console.log(reqBody.data.endpoint);
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;
Loading