request.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. var util = require('util');
  2. var events = require('events');
  3. var http = require('http');
  4. var https = require('https');
  5. var Logger = require('./../util/logger');
  6. var format = util.format;
  7. module.exports = (function() {
  8. var Settings = {
  9. selenium_host : 'localhost',
  10. selenium_port : 4444,
  11. default_path : '/wd/hub',
  12. credentials : null,
  13. use_ssl : false,
  14. proxy : null,
  15. timeout : 60000,
  16. retry_attempts : 0
  17. };
  18. var DO_NOT_LOG_ERRORS = [
  19. 'Unable to locate element',
  20. '{"errorMessage":"Unable to find element',
  21. 'no such element'
  22. ];
  23. function HttpRequest(options) {
  24. events.EventEmitter.call(this);
  25. this.setOptions(options);
  26. }
  27. util.inherits(HttpRequest, events.EventEmitter);
  28. HttpRequest.prototype.setOptions = function(options) {
  29. this.data = options.data && jsonStringify(options.data) || '';
  30. this.contentLength = this.data.length;
  31. this.reqOptions = this.createOptions(options);
  32. this.hostname = formatHostname(this.reqOptions.host, this.reqOptions.port);
  33. this.request = null;
  34. this.timeout = Settings.timeout;
  35. this.retryAttempts = Settings.retry_attempts;
  36. return this;
  37. };
  38. HttpRequest.prototype.setPathPrefix = function(options) {
  39. this.defaultPathPrefix = options.path && options.path.indexOf(Settings.default_path) === -1 ?
  40. Settings.default_path : '';
  41. return this;
  42. };
  43. HttpRequest.prototype.createOptions = function(options) {
  44. this.setPathPrefix(options);
  45. var reqOptions = {
  46. path : this.defaultPathPrefix + (options.path || ''),
  47. host : options.host || Settings.selenium_host,
  48. port : options.selenium_port || Settings.selenium_port,
  49. method : options.method || 'POST',
  50. headers : {}
  51. };
  52. var requestMethod = reqOptions.method.toUpperCase();
  53. if (options.sessionId) {
  54. reqOptions.path = reqOptions.path.replace(':sessionId', options.sessionId);
  55. }
  56. if (requestMethod === 'GET') {
  57. reqOptions.headers['Accept'] = 'application/json';
  58. }
  59. if (this.contentLength > 0) {
  60. reqOptions.headers['Content-Type'] = 'application/json; charset=utf-8';
  61. }
  62. if (needsContentLengthHeader(requestMethod)) {
  63. reqOptions.headers['Content-Length'] = this.contentLength;
  64. }
  65. if (Settings.credentials &&
  66. Settings.credentials.username && Settings.credentials.key
  67. ) {
  68. var authHeader = new Buffer(Settings.credentials.username + ':' + Settings.credentials.key).toString('base64');
  69. reqOptions.headers['Authorization'] = 'Basic ' + authHeader;
  70. }
  71. if (Settings.proxy) {
  72. var ProxyAgent = require('proxy-agent');
  73. var proxyUri = Settings.proxy;
  74. reqOptions.agent = new ProxyAgent(proxyUri);
  75. }
  76. return reqOptions;
  77. };
  78. HttpRequest.prototype.send = function() {
  79. var self = this;
  80. var startTime = new Date();
  81. var isAborted = false;
  82. this.request = (Settings.use_ssl ? https: http).request(this.reqOptions, function (response) {
  83. response.setEncoding('utf8');
  84. var redirected = false;
  85. if (isRedirect(response.statusCode)) {
  86. redirected = true;
  87. }
  88. var flushed = '';
  89. response.on('data', function (chunk) {
  90. if (self.reqOptions.method !== 'HEAD') {
  91. flushed += chunk;
  92. }
  93. });
  94. response.on('end', function () {
  95. var elapsedTime = new Date() - startTime;
  96. var screenshotContent;
  97. var result, errorMessage = '';
  98. if (flushed) {
  99. result = parseResult(flushed);
  100. if (result.value) {
  101. if (result.value.screen) {
  102. screenshotContent = result.value.screen;
  103. delete result.value.screen;
  104. }
  105. if (result.value.stackTrace) {
  106. // Selenium stack traces won't help us here and they will pollute the output
  107. delete result.value.stackTrace;
  108. }
  109. if (result.value.stacktrace) {
  110. delete result.value.stacktrace;
  111. }
  112. if (needsFormattedErrorMessage(result)) {
  113. errorMessage = formatErrorMessage(result.value);
  114. delete result.value.localizedMessage;
  115. delete result.value.message;
  116. }
  117. }
  118. } else {
  119. result = {};
  120. }
  121. if (errorMessage !== '') {
  122. console.log(Logger.colors.yellow('There was an error while executing the Selenium command') +
  123. (!Logger.isEnabled() ? ' - enabling the --verbose option might offer more details.' : '')
  124. );
  125. console.log(errorMessage);
  126. }
  127. self.emit('beforeResult', result);
  128. var base64Data;
  129. if (result.suppressBase64Data) {
  130. base64Data = result.value;
  131. result.value = '';
  132. }
  133. var logMethod = response.statusCode.toString().indexOf('5') === 0 ? 'error' : 'info';
  134. Logger[logMethod](util.format('Response %s %s %s (%sms) ', response.statusCode, self.reqOptions.method, self.hostname + self.reqOptions.path, elapsedTime), result);
  135. if (result.suppressBase64Data) {
  136. result.value = base64Data;
  137. }
  138. if (response.statusCode.toString().indexOf('2') === 0 || redirected) {
  139. if (isAborted) {
  140. return;
  141. }
  142. self.emit('success', result, response, redirected);
  143. } else {
  144. self.emit('error', result, response, screenshotContent);
  145. }
  146. });
  147. });
  148. this.request.on('error', function(response) {
  149. self.emit('error', {}, response);
  150. });
  151. this.request.setTimeout(this.timeout, function() {
  152. if (self.retryAttempts) {
  153. self.request.socket.unref();
  154. isAborted = true; // prevent emitting of the success event multiple times.
  155. self.retryAttempts = self.retryAttempts - 1;
  156. self.send();
  157. } else {
  158. self.request.abort();
  159. }
  160. });
  161. Logger.info('Request: ' + this.reqOptions.method + ' ' + this.hostname + this.reqOptions.path,
  162. '\n - data: ', this.data, '\n - headers: ', JSON.stringify(this.reqOptions.headers));
  163. this.request.write(this.data);
  164. this.request.end();
  165. return this;
  166. };
  167. /**
  168. *
  169. * @param s
  170. * @param emit_unicode
  171. * @returns {string}
  172. */
  173. HttpRequest.JSON_stringify = function(s, emit_unicode) {
  174. var json = JSON.stringify(s);
  175. if (json) {
  176. return emit_unicode ? json : json.replace(jsonRegex, jsonRegexReplace);
  177. }
  178. };
  179. HttpRequest.setSeleniumPort = function(port) {
  180. Settings.selenium_port = port;
  181. };
  182. HttpRequest.useSSL = function(value) {
  183. Settings.use_ssl = value;
  184. };
  185. HttpRequest.setSeleniumHost = function(host) {
  186. Settings.selenium_host = host;
  187. };
  188. HttpRequest.setCredentials = function(credentials) {
  189. Settings.credentials = credentials;
  190. };
  191. HttpRequest.setProxy = function(proxy) {
  192. Settings.proxy = proxy;
  193. };
  194. HttpRequest.setDefaultPathPrefix = function(path) {
  195. Settings.default_path = path;
  196. };
  197. HttpRequest.setTimeout = function(timeout) {
  198. Settings.timeout = timeout;
  199. };
  200. HttpRequest.setRetryAttempts = function(retryAttempts) {
  201. Settings.retry_attempts = retryAttempts;
  202. };
  203. ///////////////////////////////////////////////////////////
  204. // Helpers
  205. ///////////////////////////////////////////////////////////
  206. var jsonRegex = new RegExp('[\\u007f-\\uffff]', 'g');
  207. var jsonRegexReplace = function(c) {
  208. return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
  209. };
  210. /**
  211. * Built in JSON.stringify() will return unicode characters that require UTF-8 encoding on the wire.
  212. * This function will replace unicode characters with their escaped (ASCII-safe) equivalents to support
  213. * the keys sending command.
  214. *
  215. * @param {object} s
  216. * @returns {string}
  217. */
  218. function jsonStringify(s) {
  219. var json = JSON.stringify(s);
  220. if (json) {
  221. return json.replace(jsonRegex, jsonRegexReplace);
  222. }
  223. return json;
  224. }
  225. function formatHostname(hostname, port) {
  226. var isLocalHost = ['127.0.0.1', 'localhost'].indexOf(hostname) > -1;
  227. var protocol = Settings.use_ssl ? 'https://' : 'http://';
  228. var isPortDefault = [80, 443].indexOf(port) > -1;
  229. if (isLocalHost) {
  230. return '';
  231. }
  232. return protocol + hostname + (!isPortDefault && (':' + port) || '');
  233. }
  234. function isRedirect(statusCode) {
  235. return [302, 303, 304].indexOf(statusCode) > -1;
  236. }
  237. function needsContentLengthHeader(requestMethod) {
  238. return ['POST', 'DELETE'].indexOf(requestMethod) > -1;
  239. }
  240. function needsFormattedErrorMessage(result) {
  241. return !!(result.localizedMessage || result.message);
  242. }
  243. function hasLocalizedMessage(result) {
  244. return !!result.localizedMessage;
  245. }
  246. function formatErrorMessage(info) {
  247. var msg = hasLocalizedMessage(info) ? info.localizedMessage : info.message;
  248. if (shouldLogErrorMessage(msg)) {
  249. msg = msg.replace(/\n/g, '\n\t');
  250. }
  251. return msg;
  252. }
  253. function parseResult(data) {
  254. var result;
  255. data = stripUnknownChars(data);
  256. try {
  257. result = JSON.parse(data);
  258. } catch (err) {
  259. console.log(Logger.colors.red('Error processing the server response:'), '\n', data);
  260. result = {value: -1, error: err.message};
  261. }
  262. return result;
  263. }
  264. function shouldLogErrorMessage(msg) {
  265. return !DO_NOT_LOG_ERRORS.some(function(item) {
  266. return msg.indexOf(item) === 0;
  267. });
  268. }
  269. function stripUnknownChars(str) {
  270. var x = [], i = 0, length = str.length;
  271. for (i; i < length; i++) {
  272. if (str.charCodeAt(i)) {
  273. x.push(str.charAt(i));
  274. }
  275. }
  276. return x.join('');
  277. }
  278. return HttpRequest;
  279. })();