var util = require('util'); var events = require('events'); var http = require('http'); var https = require('https'); var Logger = require('./../util/logger'); var format = util.format; module.exports = (function() { var Settings = { selenium_host : 'localhost', selenium_port : 4444, default_path : '/wd/hub', credentials : null, use_ssl : false, proxy : null }; var DO_NOT_LOG_ERRORS = [ 'Unable to locate element', '{"errorMessage":"Unable to find element', 'no such element' ]; function HttpRequest(options) { events.EventEmitter.call(this); this.setOptions(options); } util.inherits(HttpRequest, events.EventEmitter); HttpRequest.prototype.setOptions = function(options) { this.data = options.data && jsonStringify(options.data) || ''; this.contentLength = this.data.length; this.reqOptions = this.createOptions(options); this.hostname = formatHostname(this.reqOptions.host, this.reqOptions.port); this.request = null; return this; }; HttpRequest.prototype.setPathPrefix = function(options) { this.defaultPathPrefix = options.path && options.path.indexOf(Settings.default_path) === -1 ? Settings.default_path : ''; return this; }; HttpRequest.prototype.createOptions = function(options) { this.setPathPrefix(options); var reqOptions = { path : this.defaultPathPrefix + (options.path || ''), host : options.host || Settings.selenium_host, port : options.selenium_port || Settings.selenium_port, method : options.method || 'POST', headers : {} }; var requestMethod = reqOptions.method.toUpperCase(); if (options.sessionId) { reqOptions.path = reqOptions.path.replace(':sessionId', options.sessionId); } if (requestMethod === 'GET') { reqOptions.headers['Accept'] = 'application/json'; } if (this.contentLength > 0) { reqOptions.headers['Content-Type'] = 'application/json; charset=utf-8'; } if (needsContentLengthHeader(requestMethod)) { reqOptions.headers['Content-Length'] = this.contentLength; } if (Settings.credentials && Settings.credentials.username && Settings.credentials.key ) { var authHeader = new Buffer(Settings.credentials.username + ':' + Settings.credentials.key).toString('base64'); reqOptions.headers['Authorization'] = 'Basic ' + authHeader; } if (Settings.proxy) { var ProxyAgent = require('proxy-agent'); var proxyUri = Settings.proxy; reqOptions.agent = new ProxyAgent(proxyUri); } return reqOptions; }; HttpRequest.prototype.send = function() { var self = this; var startTime = new Date(); this.request = (Settings.use_ssl ? https: http).request(this.reqOptions, function (response) { response.setEncoding('utf8'); var redirected = false; if (isRedirect(response.statusCode)) { redirected = true; } var flushed = ''; response.on('data', function (chunk) { if (self.reqOptions.method !== 'HEAD') { flushed += chunk; } }); response.on('end', function () { var elapsedTime = new Date() - startTime; var screenshotContent; var result, errorMessage = ''; if (flushed) { result = parseResult(flushed); if (result.value) { if (result.value.screen) { screenshotContent = result.value.screen; delete result.value.screen; } if (result.value.stackTrace) { // Selenium stack traces won't help us here and they will pollute the output delete result.value.stackTrace; } if (needsFormattedErrorMessage(result)) { errorMessage = formatErrorMessage(result.value); delete result.value.localizedMessage; delete result.value.message; } } } else { result = {}; } if (errorMessage !== '') { console.log(Logger.colors.yellow('There was an error while executing the Selenium command') + (!Logger.isEnabled() ? ' - enabling the --verbose option might offer more details.' : '') ); console.log(errorMessage); } self.emit('beforeResult', result); var base64Data; if (result.suppressBase64Data) { base64Data = result.value; result.value = ''; } var logMethod = response.statusCode.toString().indexOf('5') === 0 ? 'error' : 'info'; Logger[logMethod](util.format('Response %s %s %s (%sms) ', response.statusCode, self.reqOptions.method, self.hostname + self.reqOptions.path, elapsedTime), result); if (result.suppressBase64Data) { result.value = base64Data; } if (response.statusCode.toString().indexOf('2') === 0 || redirected) { self.emit('success', result, response, redirected); } else { self.emit('error', result, response, screenshotContent); } }); }); this.request.on('error', function(response) { self.emit('error', {}, response); }); Logger.info('Request: ' + this.reqOptions.method + ' ' + this.hostname + this.reqOptions.path, '\n - data: ', this.data, '\n - headers: ', JSON.stringify(this.reqOptions.headers)); this.request.write(this.data); this.request.end(); return this; }; /** * * @param s * @param emit_unicode * @returns {string} */ HttpRequest.JSON_stringify = function(s, emit_unicode) { var json = JSON.stringify(s); if (json) { return emit_unicode ? json : json.replace(jsonRegex, jsonRegexReplace); } }; HttpRequest.setSeleniumPort = function(port) { Settings.selenium_port = port; }; HttpRequest.useSSL = function(value) { Settings.use_ssl = value; }; HttpRequest.setSeleniumHost = function(host) { Settings.selenium_host = host; }; HttpRequest.setCredentials = function(credentials) { Settings.credentials = credentials; }; HttpRequest.setProxy = function(proxy) { Settings.proxy = proxy; }; HttpRequest.setDefaultPathPrefix = function(path) { Settings.default_path = path; }; /////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////// var jsonRegex = new RegExp('[\\u007f-\\uffff]', 'g'); var jsonRegexReplace = function(c) { return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4); }; /** * Built in JSON.stringify() will return unicode characters that require UTF-8 encoding on the wire. * This function will replace unicode characters with their escaped (ASCII-safe) equivalents to support * the keys sending command. * * @param {object} s * @returns {string} */ function jsonStringify(s) { var json = JSON.stringify(s); if (json) { return json.replace(jsonRegex, jsonRegexReplace); } return json; } function formatHostname(hostname, port) { var isLocalHost = ['127.0.0.1', 'localhost'].indexOf(hostname) > -1; var protocol = Settings.use_ssl ? 'https://' : 'http://'; var isPortDefault = [80, 443].indexOf(port) > -1; if (isLocalHost) { return ''; } return protocol + hostname + (!isPortDefault && (':' + port) || ''); } function isRedirect(statusCode) { return [302, 303, 304].indexOf(statusCode) > -1; } function needsContentLengthHeader(requestMethod) { return ['POST', 'DELETE'].indexOf(requestMethod) > -1; } function needsFormattedErrorMessage(result) { return !!(result.localizedMessage || result.message); } function hasLocalizedMessage(result) { return !!result.localizedMessage; } function formatErrorMessage(info) { var msg = hasLocalizedMessage(info) ? info.localizedMessage : info.message; if (shouldLogErrorMessage(msg)) { msg = msg.replace(/\n/g, '\n\t'); } return msg; } function parseResult(data) { var result; data = stripUnknownChars(data); try { result = JSON.parse(data); } catch (err) { console.log(Logger.colors.red('Error processing the server response:'), '\n', data); result = {value: -1, error: err.message}; } return result; } function shouldLogErrorMessage(msg) { return !DO_NOT_LOG_ERRORS.some(function(item) { return msg.indexOf(item) === 0; }); } function stripUnknownChars(str) { var x = [], i = 0, length = str.length; for (i; i < length; i++) { if (str.charCodeAt(i)) { x.push(str.charAt(i)); } } return x.join(''); } return HttpRequest; })();