Skip to content

Commit

Permalink
added network throttling and user agent device parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
bmcminn committed Apr 1, 2016
1 parent fa53222 commit 150cd25
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 33 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "electron-har",
"description": "A command-line tool for generating HTTP Archive (HAR) (based on Electron)",
"version": "0.1.8",
"version": "0.2.0",
"author": "Stanley Shyiko <[email protected]>",
"license": "MIT",
"repository": {
Expand All @@ -12,7 +12,7 @@
"main": "./src/index.js",
"dependencies": {
"cross-exec-file": "^1.0.0",
"electron-prebuilt": "^0.35.4",
"electron-prebuilt": "^0.35.6",
"json-stable-stringify": "^1.0.0",
"object-assign": "^4.0.1",
"tmp": "^0.0.28",
Expand Down
76 changes: 69 additions & 7 deletions src/electron-har.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,65 @@
var devices = require('./profiles/devices');
var networks = require('./profiles/networks');

var yargs = require('yargs')
.usage('Usage: electron-har [options...] <url>')

// NOTE: when adding an option - keep it compatible with `curl` (if possible)
.describe('u', 'Username and password (divided by colon)').alias('u', 'user').nargs('u', 1)
.describe('o', 'Write to file instead of stdout').alias('o', 'output').nargs('o', 1)
.describe('m', 'Maximum time allowed for HAR generation (in seconds)').alias('m', 'max-time').nargs('m', 1)
.describe('debug', 'Show GUI (useful for debugging)').boolean('debug')
// - https://curl.haxx.se/docs/manual.html

.describe('u', 'Username and password (divided by colon)')
.alias('u', 'user')
.nargs('u', 1)

.describe('o', 'Write to file instead of stdout')
.alias('o', 'output')
.nargs('o', 1)

.describe('m', 'Maximum time allowed for HAR generation (in seconds)')
.alias('m', 'max-time')
.nargs('m', 1)

.describe('A', 'User Agent profile to use:\n - ' + Object.keys(devices).join('\n - '))
.alias('A', 'user-agent')
.nargs('A', 1)

.describe('Y', 'Network throttling profile to use:\n - ' + Object.keys(networks).join('\n - '))
.alias('Y', 'limit-rate')
.nargs('Y', 1)

.describe('debug', 'Show GUI (useful for debugging)')
.boolean('debug')
.help('h').alias('h', 'help')
.version(function () { return require('../package').version; })
.strict();

var argv = process.env.ELECTRON_HAR_AS_NPM_MODULE ?
yargs.argv : yargs.parse(process.argv.slice(1));

var url = argv._[0];

if (argv.u) {
var usplit = argv.u.split(':');
var username = usplit[0];
var password = usplit[1] || '';
}

var outputFile = argv.output;
var timeout = parseInt(argv.m, 10);
var debug = !!argv.debug;
var userAgent = argv.A;
var limitRate = argv.Y;

var argvValidationError;

if (!url) {
argvValidationError = 'URL must be specified';

} else if (!/^(http|https):\/\//.test(url)) {
argvValidationError = 'URL must contain the protocol prefix, e.g. http://';

}

if (argvValidationError) {
yargs.showHelp();
console.error(argvValidationError);
Expand Down Expand Up @@ -76,11 +109,25 @@ electron.ipcMain
});

app.on('ready', function () {

BrowserWindow.removeDevToolsExtension('devtools-extension');
BrowserWindow.addDevToolsExtension(__dirname + '/devtools-extension');

var bw = new BrowserWindow({show: debug});
// BrowserWindow config object to store configurable properties
var winConfig = {
show: debug
};

// if a user agent profile has been selected, apply it
if (userAgent && devices[userAgent]) {
winConfig.device = devices[userAgent];

// override browser window dimensions with device specs
winConfig.width = winConfig.device.width;
winConfig.height = winConfig.device.height;
}

// initialize the browser window instance
var bw = new BrowserWindow(winConfig);

if (username) {
bw.webContents.on('login', function (event, request, authInfo, cb) {
Expand All @@ -89,6 +136,22 @@ app.on('ready', function () {
});
}

// assign the user agent string
if (winConfig.device) {

// TODO: figure out how to apply custom user-agent header to session
// bw.webContents.session.defaultSession.onBeforeSendHeaders(function(details, callback) {
// details.requestHeaders['User-Agent'] = winConfig.device.userAgentString;
// callback({cancel: false, requestHeaders: details.requestHeaders});
// });
}

// set the network throttling profile
if (limitRate) {
bw.webContents.session.enableNetworkEmulation(networks[limitRate]);
// console.log(networks[limitRate]);
}

function notifyDevToolsExtensionOfLoad(e) {
if (e.sender.getURL() != 'chrome://ensure-electron-resolution/') {
bw.webContents.executeJavaScript('new Image().src = "https://did-finish-load/"');
Expand Down Expand Up @@ -118,5 +181,4 @@ app.on('ready', function () {
// any url will do, but make sure to call loadURL before 'devtools-opened'.
// otherwise require('electron') within child BrowserWindow will (most likely) fail
bw.loadURL('chrome://ensure-electron-resolution/');

});
72 changes: 48 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
var tmp = require('tmp');
var assign = require('object-assign');
var fs = require('fs');
var execFile = require('child_process').execFile;
var crossExecFile = require('cross-exec-file');

/**
Expand All @@ -10,49 +9,74 @@ var crossExecFile = require('cross-exec-file');
* @param {object|string} o.user either object with 'name' and 'password' properties or a string (e.g. 'username:password')
* @param {string} o.user.name username
* @param {string} o.user.password password
* @param {function(err, json)} cb callback (NOTE: if err != null err.code will be the exit code (e.g. 3 - wrong usage,
* @param {function(err, json)} callback callback (NOTE: if err != null err.code will be the exit code (e.g. 3 - wrong usage,
* 4 - timeout, below zero - http://src.chromium.org/svn/trunk/src/net/base/net_error_list.h))
*/
module.exports = function electronHAR(url, o, cb) {
typeof o === 'function' && (cb = o, o = {});
// using temporary file to prevent messages like "Xlib: extension ...", "libGL error ..."
// from cluttering stdout in a headless env (as in Xvfb).
module.exports = function electronHAR(url, options, callback) {
typeof options === 'function' && (callback = options, options = {});

// using temporary file to prevent messages like "Xlib: extension ...",
// "libGL error ..." from cluttering stdout in a headless env (as in Xvfb).
tmp.file(function (err, path, fd, cleanup) {

if (err) {
return cb(err);
return callback(err);
}
cb = (function (cb) { return function () {
process.nextTick(Function.prototype.bind.apply(cb,
[null].concat(Array.prototype.slice.call(arguments))));
cleanup();
}; })(cb);
var oo = assign({}, o, {

callback = (function (callback) {
return function () {
process.nextTick(Function.prototype.bind.apply(callback,
[null].concat(Array.prototype.slice.call(arguments))
));

cleanup();
};
})(callback);

console.log(options);

// map options into config object
var config = assign({}, options, {
output: path,
user: o.user === Object(o.user) ?
o.user.name + ':' + o.user.password : o.user
user: options.user === Object(options.user) ?
options.user.name + ':' + options.user.password :
options.user,
'user-agent': options['user-agent'] ? options['user-agent'] : null,
'limit-rate': options['limit-rate'] ? options['limit-rate'] : null
});

// initialize electron-har process
crossExecFile(
__dirname + '/../bin/electron-har',
[url].concat(Object.keys(oo).reduce(function (r, k) {
var v = oo[k];
v != null && r.push(k.length === 1 ? '-' + k : '--' + k, v);
return r;
}, [])),
[url].concat(
Object
.keys(config)
.reduce(function (n, flag) {
var argvs = config[flag];
argvs !== null && argvs.push(flag.length === 1 ? '-' + flag : '--' + flag, argvs);
return argvs;
}, [])
),
function (err, stdout, stderr) {
if (err) {
if (stderr) {
err.message = stderr.trim();
}
return cb(err);

return callback(err);
}

fs.readFile(path, 'utf8', function (err, data) {
if (err) {
return cb(err);
return callback(err);
}

try {
cb(null, JSON.parse(data));
callback(null, JSON.parse(data));

} catch (e) {
return cb(e);
return callback(e);

}
});
});
Expand Down
82 changes: 82 additions & 0 deletions src/profiles/devices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module.exports = {
GALAXY_S5: {
diviceName: 'Galaxy S5',
width: 360,
height: 640,
pixelRatio: 2,
userAgentString: 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36',
useragentType: 'mobile'
},
NEXUS_5X: {
diviceName: 'Nexus 5X',
width: 411,
height: 731,
pixelRatio: 2.625,
userAgentString: 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36',
useragentType: 'mobile'
},
NEXUS_6P: {
diviceName: 'Nexus 6P',
width: 435,
height: 773,
pixelRatio: 3.3,
userAgentString: 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36',
useragentType: 'mobile'
},
IPHONE_5: {
diviceName: 'iPhone 5',
width: 320,
height: 568,
pixelRatio: 2,
userAgentString: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
useragentType: 'mobile'
},
IPHONE_6: {
diviceName: 'iPhone 6',
width: 375,
height: 667,
pixelRatio: 2,
userAgentString: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
useragentType: 'mobile'
},
IPHONE_6PLUS: {
diviceName: 'iPhone 6+',
width: 414,
height: 736,
pixelRatio: 3,
userAgentString: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
useragentType: 'mobile'
},
IPAD: {
diviceName: 'iPad',
width: 768,
height: 1024,
pixelRatio: 2,
userAgentString: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
useragentType: 'mobile'
},
DESKTOP_1080P: {
diviceName: 'Desktop 1080p',
width: 1920,
height: 1080,
pixelRatio: 1,
userAgentString: '',
useragentType: 'desktop'
},
LAPTOP_1080P: {
diviceName: 'Laptop Touch screen',
width: 1920,
height: 1080,
pixelRatio: 1,
userAgentString: '',
useragentType: 'desktop with touch'
}
// IPAD_PRO: {
// diviceName: 2,
// width: 30000,
// height: 15000,
// pixelRatio: '',
// userAgentString: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
// useragentType: 'mobile'
// }
};
51 changes: 51 additions & 0 deletions src/profiles/networks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
var Mb = 100000; // 1 million Bytes in a Megabyte
var Kb = 1000; // 1 thousand Bytes in a Kilobyte


/**
* List of enumerated latency profiles
* @sauce: https://github.com/atom/electron/blob/master/docs/api/session.md#sesenablenetworkemulationoptions
* @type {Object}
*/
module.exports = {
WIFI: {
latency: 2,
downloadThroughput: 30*Mb,
uploadThroughput: 15*Mb
},
DSL: {
latency: 5,
downloadThroughput: 2*Mb,
uploadThroughput: 1*Mb
},
REGULAR_4G: {
latency: 20,
downloadThroughput: 4*Mb,
uploadThroughput: 3*Mb
},
GOOD_3G: {
latency: 40,
downloadThroughput: 1*Mb,
uploadThroughput: 750*Kb
},
REGULAR_3G: {
latency: 100,
downloadThroughput: 750*Kb,
uploadThroughput: 250*Kb
},
GOOD_2G: {
latency: 150,
downloadThroughput: 450*Kb,
uploadThroughput: 150*Kb
},
REGULAR_2G: {
latency: 300,
downloadThroughput: 250*Kb,
uploadThroughput: 50*Kb
},
GPRS: {
latency: 500,
downloadThroughput: 50*Kb,
uploadThroughput: 20*Kb,
}
};

0 comments on commit 150cd25

Please sign in to comment.