Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/http/index.js
3220 views
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
/**
19
* @fileoverview Defines an {@linkplain cmd.Executor command executor} that
20
* communicates with a remote end using HTTP + JSON.
21
*/
22
23
'use strict'
24
25
const http = require('node:http')
26
const https = require('node:https')
27
const url = require('node:url')
28
29
const httpLib = require('../lib/http')
30
31
/**
32
* @typedef {{protocol: (?string|undefined),
33
* auth: (?string|undefined),
34
* hostname: (?string|undefined),
35
* host: (?string|undefined),
36
* port: (?string|undefined),
37
* path: (?string|undefined),
38
* pathname: (?string|undefined)}}
39
*/
40
let RequestOptions // eslint-disable-line
41
42
/**
43
* @param {string} aUrl The request URL to parse.
44
* @return {RequestOptions} The request options.
45
* @throws {Error} if the URL does not include a hostname.
46
*/
47
function getRequestOptions(aUrl) {
48
// eslint-disable-next-line n/no-deprecated-api
49
let options = url.parse(aUrl)
50
if (!options.hostname) {
51
throw new Error('Invalid URL: ' + aUrl)
52
}
53
// Delete the search and has portions as they are not used.
54
options.search = null
55
options.hash = null
56
options.path = options.pathname
57
options.hostname = options.hostname === 'localhost' ? '127.0.0.1' : options.hostname // To support Node 17 and above. Refer https://github.com/nodejs/node/issues/40702 for details.
58
return options
59
}
60
61
/** @const {string} */
62
const USER_AGENT = (function () {
63
const version = require('../package.json').version
64
const platform = { darwin: 'mac', win32: 'windows' }[process.platform] || 'linux'
65
return `selenium/${version} (js ${platform})`
66
})()
67
68
/**
69
* A basic HTTP client used to send messages to a remote end.
70
*
71
* @implements {httpLib.Client}
72
*/
73
class HttpClient {
74
/**
75
* @param {string} serverUrl URL for the WebDriver server to send commands to.
76
* @param {http.Agent=} opt_agent The agent to use for each request.
77
* Defaults to `http.globalAgent`.
78
* @param {?string=} opt_proxy The proxy to use for the connection to the
79
* server. Default is to use no proxy.
80
* @param {?Object.<string,Object>} client_options
81
*/
82
constructor(serverUrl, opt_agent, opt_proxy, client_options = {}) {
83
/** @private {http.Agent} */
84
this.agent_ = opt_agent || null
85
86
/**
87
* Base options for each request.
88
* @private {RequestOptions}
89
*/
90
this.options_ = getRequestOptions(serverUrl)
91
92
/**
93
* client options, header overrides
94
*/
95
this.client_options = client_options
96
97
/**
98
* sets keep-alive for the agent
99
* see https://stackoverflow.com/a/58332910
100
*/
101
this.keepAlive = this.client_options['keep-alive']
102
103
/** @private {?RequestOptions} */
104
this.proxyOptions_ = opt_proxy ? getRequestOptions(opt_proxy) : null
105
}
106
107
get keepAlive() {
108
return this.agent_.keepAlive
109
}
110
111
set keepAlive(value) {
112
if (value === 'true' || value === true) {
113
this.agent_.keepAlive = true
114
}
115
}
116
117
/** @override */
118
send(httpRequest) {
119
let data
120
121
let headers = {}
122
123
if (httpRequest.headers) {
124
httpRequest.headers.forEach(function (value, name) {
125
headers[name] = value
126
})
127
}
128
129
headers['User-Agent'] = this.client_options['user-agent'] || USER_AGENT
130
headers['Content-Length'] = 0
131
if (httpRequest.method == 'POST' || httpRequest.method == 'PUT') {
132
data = JSON.stringify(httpRequest.data)
133
headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
134
headers['Content-Type'] = 'application/json;charset=UTF-8'
135
}
136
137
let path = this.options_.path
138
if (path.endsWith('/') && httpRequest.path.startsWith('/')) {
139
path += httpRequest.path.substring(1)
140
} else {
141
path += httpRequest.path
142
}
143
// eslint-disable-next-line n/no-deprecated-api
144
let parsedPath = url.parse(path)
145
146
let options = {
147
agent: this.agent_ || null,
148
method: httpRequest.method,
149
150
auth: this.options_.auth,
151
hostname: this.options_.hostname,
152
port: this.options_.port,
153
protocol: this.options_.protocol,
154
155
path: parsedPath.path,
156
pathname: parsedPath.pathname,
157
search: parsedPath.search,
158
hash: parsedPath.hash,
159
160
headers,
161
}
162
163
return new Promise((fulfill, reject) => {
164
sendRequest(options, fulfill, reject, data, this.proxyOptions_)
165
})
166
}
167
}
168
169
/**
170
* Sends a single HTTP request.
171
* @param {!Object} options The request options.
172
* @param {function(!httpLib.Response)} onOk The function to call if the
173
* request succeeds.
174
* @param {function(!Error)} onError The function to call if the request fails.
175
* @param {?string=} opt_data The data to send with the request.
176
* @param {?RequestOptions=} opt_proxy The proxy server to use for the request.
177
* @param {number=} opt_retries The current number of retries.
178
*/
179
function sendRequest(options, onOk, onError, opt_data, opt_proxy, opt_retries) {
180
var hostname = options.hostname
181
var port = options.port
182
183
if (opt_proxy) {
184
let proxy = /** @type {RequestOptions} */ (opt_proxy)
185
186
// RFC 2616, section 5.1.2:
187
// The absoluteURI form is REQUIRED when the request is being made to a
188
// proxy.
189
let absoluteUri = url.format(options)
190
191
// RFC 2616, section 14.23:
192
// An HTTP/1.1 proxy MUST ensure that any request message it forwards does
193
// contain an appropriate Host header field that identifies the service
194
// being requested by the proxy.
195
let targetHost = options.hostname
196
if (options.port) {
197
targetHost += ':' + options.port
198
}
199
200
// Update the request options with our proxy info.
201
options.headers['Host'] = targetHost
202
options.path = absoluteUri
203
options.host = proxy.host
204
options.hostname = proxy.hostname
205
options.port = proxy.port
206
207
// Update the protocol to avoid EPROTO errors when the webdriver proxy
208
// uses a different protocol from the remote selenium server.
209
options.protocol = opt_proxy.protocol
210
211
if (proxy.auth) {
212
options.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64')
213
}
214
}
215
216
let requestFn = options.protocol === 'https:' ? https.request : http.request
217
var request = requestFn(options, function onResponse(response) {
218
if (response.statusCode == 302 || response.statusCode == 303) {
219
let location
220
try {
221
// eslint-disable-next-line n/no-deprecated-api
222
location = url.parse(response.headers['location'])
223
} catch (ex) {
224
onError(
225
Error(
226
'Failed to parse "Location" header for server redirect: ' +
227
ex.message +
228
'\nResponse was: \n' +
229
new httpLib.Response(response.statusCode, response.headers, ''),
230
),
231
)
232
return
233
}
234
235
if (!location.hostname) {
236
location.hostname = hostname
237
location.port = port
238
location.auth = options.auth
239
}
240
241
request.destroy()
242
sendRequest(
243
{
244
method: 'GET',
245
protocol: location.protocol || options.protocol,
246
hostname: location.hostname,
247
port: location.port,
248
path: location.path,
249
auth: location.auth,
250
pathname: location.pathname,
251
search: location.search,
252
hash: location.hash,
253
headers: {
254
Accept: 'application/json; charset=utf-8',
255
'User-Agent': options.headers['User-Agent'] || USER_AGENT,
256
},
257
},
258
onOk,
259
onError,
260
undefined,
261
opt_proxy,
262
)
263
return
264
}
265
266
const body = []
267
response.on('data', body.push.bind(body))
268
response.on('end', function () {
269
const resp = new httpLib.Response(
270
/** @type {number} */ (response.statusCode),
271
/** @type {!Object<string>} */ (response.headers),
272
Buffer.concat(body).toString('utf8').replace(/\0/g, ''),
273
)
274
onOk(resp)
275
})
276
})
277
278
request.on('error', function (e) {
279
if (typeof opt_retries === 'undefined') {
280
opt_retries = 0
281
}
282
283
if (shouldRetryRequest(opt_retries, e)) {
284
opt_retries += 1
285
setTimeout(function () {
286
sendRequest(options, onOk, onError, opt_data, opt_proxy, opt_retries)
287
}, 15)
288
} else {
289
let message = e.message
290
if (e.code) {
291
message = e.code + ' ' + message
292
}
293
onError(new Error(message))
294
}
295
})
296
297
if (opt_data) {
298
request.write(opt_data)
299
}
300
301
request.end()
302
}
303
304
const MAX_RETRIES = 3
305
306
/**
307
* A retry is sometimes needed on Windows where we may quickly run out of
308
* ephemeral ports. A more robust solution is bumping the MaxUserPort setting
309
* as described here: http://msdn.microsoft.com/en-us/library/aa560610%28v=bts.20%29.aspx
310
*
311
* @param {!number} retries
312
* @param {!Error} err
313
* @return {boolean}
314
*/
315
function shouldRetryRequest(retries, err) {
316
return retries < MAX_RETRIES && isRetryableNetworkError(err)
317
}
318
319
/**
320
* @param {!Error} err
321
* @return {boolean}
322
*/
323
function isRetryableNetworkError(err) {
324
if (err && err.code) {
325
return (
326
err.code === 'ECONNABORTED' ||
327
err.code === 'ECONNRESET' ||
328
err.code === 'ECONNREFUSED' ||
329
err.code === 'EADDRINUSE' ||
330
err.code === 'EPIPE' ||
331
err.code === 'ETIMEDOUT'
332
)
333
}
334
335
return false
336
}
337
338
// PUBLIC API
339
340
module.exports.Agent = http.Agent
341
module.exports.Executor = httpLib.Executor
342
module.exports.HttpClient = HttpClient
343
module.exports.Request = httpLib.Request
344
module.exports.Response = httpLib.Response
345
346