index.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // Licensed to the Software Freedom Conservancy (SFC) under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. The SFC licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing,
  12. // software distributed under the License is distributed on an
  13. // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. // KIND, either express or implied. See the License for the
  15. // specific language governing permissions and limitations
  16. // under the License.
  17. /**
  18. * @fileoverview Defines an {@linkplain cmd.Executor command executor} that
  19. * communicates with a remote end using HTTP + JSON.
  20. */
  21. 'use strict';
  22. const http = require('http');
  23. const https = require('https');
  24. const url = require('url');
  25. const httpLib = require('../lib/http');
  26. /**
  27. * @typedef {{protocol: (?string|undefined),
  28. * auth: (?string|undefined),
  29. * hostname: (?string|undefined),
  30. * host: (?string|undefined),
  31. * port: (?string|undefined),
  32. * path: (?string|undefined),
  33. * pathname: (?string|undefined)}}
  34. */
  35. var RequestOptions;
  36. /**
  37. * @param {string} aUrl The request URL to parse.
  38. * @return {RequestOptions} The request options.
  39. * @throws {Error} if the URL does not include a hostname.
  40. */
  41. function getRequestOptions(aUrl) {
  42. let options = url.parse(aUrl);
  43. if (!options.hostname) {
  44. throw new Error('Invalid URL: ' + aUrl);
  45. }
  46. // Delete the search and has portions as they are not used.
  47. options.search = null;
  48. options.hash = null;
  49. options.path = options.pathname;
  50. return options;
  51. }
  52. /**
  53. * A basic HTTP client used to send messages to a remote end.
  54. *
  55. * @implements {httpLib.Client}
  56. */
  57. class HttpClient {
  58. /**
  59. * @param {string} serverUrl URL for the WebDriver server to send commands to.
  60. * @param {http.Agent=} opt_agent The agent to use for each request.
  61. * Defaults to `http.globalAgent`.
  62. * @param {?string=} opt_proxy The proxy to use for the connection to the
  63. * server. Default is to use no proxy.
  64. */
  65. constructor(serverUrl, opt_agent, opt_proxy) {
  66. /** @private {http.Agent} */
  67. this.agent_ = opt_agent || null;
  68. /**
  69. * Base options for each request.
  70. * @private {RequestOptions}
  71. */
  72. this.options_ = getRequestOptions(serverUrl);
  73. /**
  74. * @private {?RequestOptions}
  75. */
  76. this.proxyOptions_ = opt_proxy ? getRequestOptions(opt_proxy) : null;
  77. }
  78. /** @override */
  79. send(httpRequest) {
  80. let data;
  81. let headers = {};
  82. httpRequest.headers.forEach(function(value, name) {
  83. headers[name] = value;
  84. });
  85. headers['Content-Length'] = 0;
  86. if (httpRequest.method == 'POST' || httpRequest.method == 'PUT') {
  87. data = JSON.stringify(httpRequest.data);
  88. headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
  89. headers['Content-Type'] = 'application/json;charset=UTF-8';
  90. }
  91. let path = this.options_.path;
  92. if (path.endsWith('/') && httpRequest.path.startsWith('/')) {
  93. path += httpRequest.path.substring(1);
  94. } else {
  95. path += httpRequest.path;
  96. }
  97. let parsedPath = url.parse(path);
  98. let options = {
  99. agent: this.agent_ || null,
  100. method: httpRequest.method,
  101. auth: this.options_.auth,
  102. hostname: this.options_.hostname,
  103. port: this.options_.port,
  104. protocol: this.options_.protocol,
  105. path: parsedPath.path,
  106. pathname: parsedPath.pathname,
  107. search: parsedPath.search,
  108. hash: parsedPath.hash,
  109. headers,
  110. };
  111. return new Promise((fulfill, reject) => {
  112. sendRequest(options, fulfill, reject, data, this.proxyOptions_);
  113. });
  114. }
  115. }
  116. /**
  117. * Sends a single HTTP request.
  118. * @param {!Object} options The request options.
  119. * @param {function(!httpLib.Response)} onOk The function to call if the
  120. * request succeeds.
  121. * @param {function(!Error)} onError The function to call if the request fails.
  122. * @param {?string=} opt_data The data to send with the request.
  123. * @param {?RequestOptions=} opt_proxy The proxy server to use for the request.
  124. */
  125. function sendRequest(options, onOk, onError, opt_data, opt_proxy) {
  126. var hostname = options.hostname;
  127. var port = options.port;
  128. if (opt_proxy) {
  129. let proxy = /** @type {RequestOptions} */(opt_proxy);
  130. // RFC 2616, section 5.1.2:
  131. // The absoluteURI form is REQUIRED when the request is being made to a
  132. // proxy.
  133. let absoluteUri = url.format(options);
  134. // RFC 2616, section 14.23:
  135. // An HTTP/1.1 proxy MUST ensure that any request message it forwards does
  136. // contain an appropriate Host header field that identifies the service
  137. // being requested by the proxy.
  138. let targetHost = options.hostname
  139. if (options.port) {
  140. targetHost += ':' + options.port;
  141. }
  142. // Update the request options with our proxy info.
  143. options.headers['Host'] = targetHost;
  144. options.path = absoluteUri;
  145. options.host = proxy.host;
  146. options.hostname = proxy.hostname;
  147. options.port = proxy.port;
  148. if (proxy.auth) {
  149. options.headers['Proxy-Authorization'] =
  150. 'Basic ' + new Buffer(proxy.auth).toString('base64');
  151. }
  152. }
  153. let requestFn = options.protocol === 'https:' ? https.request : http.request;
  154. var request = requestFn(options, function onResponse(response) {
  155. if (response.statusCode == 302 || response.statusCode == 303) {
  156. try {
  157. var location = url.parse(response.headers['location']);
  158. } catch (ex) {
  159. onError(Error(
  160. 'Failed to parse "Location" header for server redirect: ' +
  161. ex.message + '\nResponse was: \n' +
  162. new httpLib.Response(response.statusCode, response.headers, '')));
  163. return;
  164. }
  165. if (!location.hostname) {
  166. location.hostname = hostname;
  167. location.port = port;
  168. }
  169. request.abort();
  170. sendRequest({
  171. method: 'GET',
  172. protocol: location.protocol || options.protocol,
  173. hostname: location.hostname,
  174. port: location.port,
  175. path: location.path,
  176. pathname: location.pathname,
  177. search: location.search,
  178. hash: location.hash,
  179. headers: {
  180. 'Accept': 'application/json; charset=utf-8'
  181. }
  182. }, onOk, onError, undefined, opt_proxy);
  183. return;
  184. }
  185. var body = [];
  186. response.on('data', body.push.bind(body));
  187. response.on('end', function() {
  188. var resp = new httpLib.Response(
  189. /** @type {number} */(response.statusCode),
  190. /** @type {!Object<string>} */(response.headers),
  191. body.join('').replace(/\0/g, ''));
  192. onOk(resp);
  193. });
  194. });
  195. request.on('error', function(e) {
  196. if (e.code === 'ECONNRESET') {
  197. setTimeout(function() {
  198. sendRequest(options, onOk, onError, opt_data, opt_proxy);
  199. }, 15);
  200. } else {
  201. var message = e.message;
  202. if (e.code) {
  203. message = e.code + ' ' + message;
  204. }
  205. onError(new Error(message));
  206. }
  207. });
  208. if (opt_data) {
  209. request.write(opt_data);
  210. }
  211. request.end();
  212. }
  213. // PUBLIC API
  214. exports.Executor = httpLib.Executor;
  215. exports.HttpClient = HttpClient;
  216. exports.Request = httpLib.Request;
  217. exports.Response = httpLib.Response;