@actions/http-client 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASES.md +4 -0
- package/auth.d.ts +23 -0
- package/auth.js +58 -0
- package/index.d.ts +124 -0
- package/index.js +537 -0
- package/interfaces.d.ts +49 -0
- package/interfaces.js +2 -0
- package/package.json +1 -1
- package/proxy.d.ts +2 -0
- package/proxy.js +57 -0
- package/.github/workflows/test.yml +0 -51
- package/.prettierignore +0 -2
- package/.prettierrc.json +0 -11
- package/__tests__/auth.test.ts +0 -61
- package/__tests__/basics.test.ts +0 -375
- package/__tests__/headers.test.ts +0 -115
- package/__tests__/keepalive.test.ts +0 -79
- package/__tests__/proxy.test.ts +0 -228
- package/auth.ts +0 -86
- package/index.ts +0 -768
- package/interfaces.ts +0 -98
- package/jest.config.js +0 -10
- package/proxy.ts +0 -60
- package/tsconfig.json +0 -15
package/index.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const pm = require("./proxy");
|
|
6
|
+
let tunnel;
|
|
7
|
+
var HttpCodes;
|
|
8
|
+
(function (HttpCodes) {
|
|
9
|
+
HttpCodes[HttpCodes["OK"] = 200] = "OK";
|
|
10
|
+
HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices";
|
|
11
|
+
HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently";
|
|
12
|
+
HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved";
|
|
13
|
+
HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther";
|
|
14
|
+
HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified";
|
|
15
|
+
HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy";
|
|
16
|
+
HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy";
|
|
17
|
+
HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect";
|
|
18
|
+
HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect";
|
|
19
|
+
HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest";
|
|
20
|
+
HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized";
|
|
21
|
+
HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired";
|
|
22
|
+
HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden";
|
|
23
|
+
HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound";
|
|
24
|
+
HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed";
|
|
25
|
+
HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable";
|
|
26
|
+
HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired";
|
|
27
|
+
HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout";
|
|
28
|
+
HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict";
|
|
29
|
+
HttpCodes[HttpCodes["Gone"] = 410] = "Gone";
|
|
30
|
+
HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests";
|
|
31
|
+
HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError";
|
|
32
|
+
HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented";
|
|
33
|
+
HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway";
|
|
34
|
+
HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable";
|
|
35
|
+
HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout";
|
|
36
|
+
})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {}));
|
|
37
|
+
var Headers;
|
|
38
|
+
(function (Headers) {
|
|
39
|
+
Headers["Accept"] = "accept";
|
|
40
|
+
Headers["ContentType"] = "content-type";
|
|
41
|
+
})(Headers = exports.Headers || (exports.Headers = {}));
|
|
42
|
+
var MediaTypes;
|
|
43
|
+
(function (MediaTypes) {
|
|
44
|
+
MediaTypes["ApplicationJson"] = "application/json";
|
|
45
|
+
})(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {}));
|
|
46
|
+
/**
|
|
47
|
+
* Returns the proxy URL, depending upon the supplied url and proxy environment variables.
|
|
48
|
+
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
|
|
49
|
+
*/
|
|
50
|
+
function getProxyUrl(serverUrl) {
|
|
51
|
+
let proxyUrl = pm.getProxyUrl(new URL(serverUrl));
|
|
52
|
+
return proxyUrl ? proxyUrl.href : '';
|
|
53
|
+
}
|
|
54
|
+
exports.getProxyUrl = getProxyUrl;
|
|
55
|
+
const HttpRedirectCodes = [
|
|
56
|
+
HttpCodes.MovedPermanently,
|
|
57
|
+
HttpCodes.ResourceMoved,
|
|
58
|
+
HttpCodes.SeeOther,
|
|
59
|
+
HttpCodes.TemporaryRedirect,
|
|
60
|
+
HttpCodes.PermanentRedirect
|
|
61
|
+
];
|
|
62
|
+
const HttpResponseRetryCodes = [
|
|
63
|
+
HttpCodes.BadGateway,
|
|
64
|
+
HttpCodes.ServiceUnavailable,
|
|
65
|
+
HttpCodes.GatewayTimeout
|
|
66
|
+
];
|
|
67
|
+
const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD'];
|
|
68
|
+
const ExponentialBackoffCeiling = 10;
|
|
69
|
+
const ExponentialBackoffTimeSlice = 5;
|
|
70
|
+
class HttpClientError extends Error {
|
|
71
|
+
constructor(message, statusCode) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'HttpClientError';
|
|
74
|
+
this.statusCode = statusCode;
|
|
75
|
+
Object.setPrototypeOf(this, HttpClientError.prototype);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.HttpClientError = HttpClientError;
|
|
79
|
+
class HttpClientResponse {
|
|
80
|
+
constructor(message) {
|
|
81
|
+
this.message = message;
|
|
82
|
+
}
|
|
83
|
+
readBody() {
|
|
84
|
+
return new Promise(async (resolve, reject) => {
|
|
85
|
+
let output = Buffer.alloc(0);
|
|
86
|
+
this.message.on('data', (chunk) => {
|
|
87
|
+
output = Buffer.concat([output, chunk]);
|
|
88
|
+
});
|
|
89
|
+
this.message.on('end', () => {
|
|
90
|
+
resolve(output.toString());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.HttpClientResponse = HttpClientResponse;
|
|
96
|
+
function isHttps(requestUrl) {
|
|
97
|
+
let parsedUrl = new URL(requestUrl);
|
|
98
|
+
return parsedUrl.protocol === 'https:';
|
|
99
|
+
}
|
|
100
|
+
exports.isHttps = isHttps;
|
|
101
|
+
class HttpClient {
|
|
102
|
+
constructor(userAgent, handlers, requestOptions) {
|
|
103
|
+
this._ignoreSslError = false;
|
|
104
|
+
this._allowRedirects = true;
|
|
105
|
+
this._allowRedirectDowngrade = false;
|
|
106
|
+
this._maxRedirects = 50;
|
|
107
|
+
this._allowRetries = false;
|
|
108
|
+
this._maxRetries = 1;
|
|
109
|
+
this._keepAlive = false;
|
|
110
|
+
this._disposed = false;
|
|
111
|
+
this.userAgent = userAgent;
|
|
112
|
+
this.handlers = handlers || [];
|
|
113
|
+
this.requestOptions = requestOptions;
|
|
114
|
+
if (requestOptions) {
|
|
115
|
+
if (requestOptions.ignoreSslError != null) {
|
|
116
|
+
this._ignoreSslError = requestOptions.ignoreSslError;
|
|
117
|
+
}
|
|
118
|
+
this._socketTimeout = requestOptions.socketTimeout;
|
|
119
|
+
if (requestOptions.allowRedirects != null) {
|
|
120
|
+
this._allowRedirects = requestOptions.allowRedirects;
|
|
121
|
+
}
|
|
122
|
+
if (requestOptions.allowRedirectDowngrade != null) {
|
|
123
|
+
this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade;
|
|
124
|
+
}
|
|
125
|
+
if (requestOptions.maxRedirects != null) {
|
|
126
|
+
this._maxRedirects = Math.max(requestOptions.maxRedirects, 0);
|
|
127
|
+
}
|
|
128
|
+
if (requestOptions.keepAlive != null) {
|
|
129
|
+
this._keepAlive = requestOptions.keepAlive;
|
|
130
|
+
}
|
|
131
|
+
if (requestOptions.allowRetries != null) {
|
|
132
|
+
this._allowRetries = requestOptions.allowRetries;
|
|
133
|
+
}
|
|
134
|
+
if (requestOptions.maxRetries != null) {
|
|
135
|
+
this._maxRetries = requestOptions.maxRetries;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
options(requestUrl, additionalHeaders) {
|
|
140
|
+
return this.request('OPTIONS', requestUrl, null, additionalHeaders || {});
|
|
141
|
+
}
|
|
142
|
+
get(requestUrl, additionalHeaders) {
|
|
143
|
+
return this.request('GET', requestUrl, null, additionalHeaders || {});
|
|
144
|
+
}
|
|
145
|
+
del(requestUrl, additionalHeaders) {
|
|
146
|
+
return this.request('DELETE', requestUrl, null, additionalHeaders || {});
|
|
147
|
+
}
|
|
148
|
+
post(requestUrl, data, additionalHeaders) {
|
|
149
|
+
return this.request('POST', requestUrl, data, additionalHeaders || {});
|
|
150
|
+
}
|
|
151
|
+
patch(requestUrl, data, additionalHeaders) {
|
|
152
|
+
return this.request('PATCH', requestUrl, data, additionalHeaders || {});
|
|
153
|
+
}
|
|
154
|
+
put(requestUrl, data, additionalHeaders) {
|
|
155
|
+
return this.request('PUT', requestUrl, data, additionalHeaders || {});
|
|
156
|
+
}
|
|
157
|
+
head(requestUrl, additionalHeaders) {
|
|
158
|
+
return this.request('HEAD', requestUrl, null, additionalHeaders || {});
|
|
159
|
+
}
|
|
160
|
+
sendStream(verb, requestUrl, stream, additionalHeaders) {
|
|
161
|
+
return this.request(verb, requestUrl, stream, additionalHeaders);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Gets a typed object from an endpoint
|
|
165
|
+
* Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise
|
|
166
|
+
*/
|
|
167
|
+
async getJson(requestUrl, additionalHeaders = {}) {
|
|
168
|
+
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
|
|
169
|
+
let res = await this.get(requestUrl, additionalHeaders);
|
|
170
|
+
return this._processResponse(res, this.requestOptions);
|
|
171
|
+
}
|
|
172
|
+
async postJson(requestUrl, obj, additionalHeaders = {}) {
|
|
173
|
+
let data = JSON.stringify(obj, null, 2);
|
|
174
|
+
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
|
|
175
|
+
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
|
|
176
|
+
let res = await this.post(requestUrl, data, additionalHeaders);
|
|
177
|
+
return this._processResponse(res, this.requestOptions);
|
|
178
|
+
}
|
|
179
|
+
async putJson(requestUrl, obj, additionalHeaders = {}) {
|
|
180
|
+
let data = JSON.stringify(obj, null, 2);
|
|
181
|
+
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
|
|
182
|
+
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
|
|
183
|
+
let res = await this.put(requestUrl, data, additionalHeaders);
|
|
184
|
+
return this._processResponse(res, this.requestOptions);
|
|
185
|
+
}
|
|
186
|
+
async patchJson(requestUrl, obj, additionalHeaders = {}) {
|
|
187
|
+
let data = JSON.stringify(obj, null, 2);
|
|
188
|
+
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
|
|
189
|
+
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
|
|
190
|
+
let res = await this.patch(requestUrl, data, additionalHeaders);
|
|
191
|
+
return this._processResponse(res, this.requestOptions);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Makes a raw http request.
|
|
195
|
+
* All other methods such as get, post, patch, and request ultimately call this.
|
|
196
|
+
* Prefer get, del, post and patch
|
|
197
|
+
*/
|
|
198
|
+
async request(verb, requestUrl, data, headers) {
|
|
199
|
+
if (this._disposed) {
|
|
200
|
+
throw new Error('Client has already been disposed.');
|
|
201
|
+
}
|
|
202
|
+
let parsedUrl = new URL(requestUrl);
|
|
203
|
+
let info = this._prepareRequest(verb, parsedUrl, headers);
|
|
204
|
+
// Only perform retries on reads since writes may not be idempotent.
|
|
205
|
+
let maxTries = this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
|
|
206
|
+
? this._maxRetries + 1
|
|
207
|
+
: 1;
|
|
208
|
+
let numTries = 0;
|
|
209
|
+
let response;
|
|
210
|
+
while (numTries < maxTries) {
|
|
211
|
+
response = await this.requestRaw(info, data);
|
|
212
|
+
// Check if it's an authentication challenge
|
|
213
|
+
if (response &&
|
|
214
|
+
response.message &&
|
|
215
|
+
response.message.statusCode === HttpCodes.Unauthorized) {
|
|
216
|
+
let authenticationHandler;
|
|
217
|
+
for (let i = 0; i < this.handlers.length; i++) {
|
|
218
|
+
if (this.handlers[i].canHandleAuthentication(response)) {
|
|
219
|
+
authenticationHandler = this.handlers[i];
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (authenticationHandler) {
|
|
224
|
+
return authenticationHandler.handleAuthentication(this, info, data);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// We have received an unauthorized response but have no handlers to handle it.
|
|
228
|
+
// Let the response return to the caller.
|
|
229
|
+
return response;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
let redirectsRemaining = this._maxRedirects;
|
|
233
|
+
while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
|
|
234
|
+
this._allowRedirects &&
|
|
235
|
+
redirectsRemaining > 0) {
|
|
236
|
+
const redirectUrl = response.message.headers['location'];
|
|
237
|
+
if (!redirectUrl) {
|
|
238
|
+
// if there's no location to redirect to, we won't
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
let parsedRedirectUrl = new URL(redirectUrl);
|
|
242
|
+
if (parsedUrl.protocol == 'https:' &&
|
|
243
|
+
parsedUrl.protocol != parsedRedirectUrl.protocol &&
|
|
244
|
+
!this._allowRedirectDowngrade) {
|
|
245
|
+
throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.');
|
|
246
|
+
}
|
|
247
|
+
// we need to finish reading the response before reassigning response
|
|
248
|
+
// which will leak the open socket.
|
|
249
|
+
await response.readBody();
|
|
250
|
+
// strip authorization header if redirected to a different hostname
|
|
251
|
+
if (parsedRedirectUrl.hostname !== parsedUrl.hostname) {
|
|
252
|
+
for (let header in headers) {
|
|
253
|
+
// header names are case insensitive
|
|
254
|
+
if (header.toLowerCase() === 'authorization') {
|
|
255
|
+
delete headers[header];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// let's make the request with the new redirectUrl
|
|
260
|
+
info = this._prepareRequest(verb, parsedRedirectUrl, headers);
|
|
261
|
+
response = await this.requestRaw(info, data);
|
|
262
|
+
redirectsRemaining--;
|
|
263
|
+
}
|
|
264
|
+
if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) {
|
|
265
|
+
// If not a retry code, return immediately instead of retrying
|
|
266
|
+
return response;
|
|
267
|
+
}
|
|
268
|
+
numTries += 1;
|
|
269
|
+
if (numTries < maxTries) {
|
|
270
|
+
await response.readBody();
|
|
271
|
+
await this._performExponentialBackoff(numTries);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return response;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Needs to be called if keepAlive is set to true in request options.
|
|
278
|
+
*/
|
|
279
|
+
dispose() {
|
|
280
|
+
if (this._agent) {
|
|
281
|
+
this._agent.destroy();
|
|
282
|
+
}
|
|
283
|
+
this._disposed = true;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Raw request.
|
|
287
|
+
* @param info
|
|
288
|
+
* @param data
|
|
289
|
+
*/
|
|
290
|
+
requestRaw(info, data) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
let callbackForResult = function (err, res) {
|
|
293
|
+
if (err) {
|
|
294
|
+
reject(err);
|
|
295
|
+
}
|
|
296
|
+
resolve(res);
|
|
297
|
+
};
|
|
298
|
+
this.requestRawWithCallback(info, data, callbackForResult);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Raw request with callback.
|
|
303
|
+
* @param info
|
|
304
|
+
* @param data
|
|
305
|
+
* @param onResult
|
|
306
|
+
*/
|
|
307
|
+
requestRawWithCallback(info, data, onResult) {
|
|
308
|
+
let socket;
|
|
309
|
+
if (typeof data === 'string') {
|
|
310
|
+
info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
|
|
311
|
+
}
|
|
312
|
+
let callbackCalled = false;
|
|
313
|
+
let handleResult = (err, res) => {
|
|
314
|
+
if (!callbackCalled) {
|
|
315
|
+
callbackCalled = true;
|
|
316
|
+
onResult(err, res);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
let req = info.httpModule.request(info.options, (msg) => {
|
|
320
|
+
let res = new HttpClientResponse(msg);
|
|
321
|
+
handleResult(null, res);
|
|
322
|
+
});
|
|
323
|
+
req.on('socket', sock => {
|
|
324
|
+
socket = sock;
|
|
325
|
+
});
|
|
326
|
+
// If we ever get disconnected, we want the socket to timeout eventually
|
|
327
|
+
req.setTimeout(this._socketTimeout || 3 * 60000, () => {
|
|
328
|
+
if (socket) {
|
|
329
|
+
socket.end();
|
|
330
|
+
}
|
|
331
|
+
handleResult(new Error('Request timeout: ' + info.options.path), null);
|
|
332
|
+
});
|
|
333
|
+
req.on('error', function (err) {
|
|
334
|
+
// err has statusCode property
|
|
335
|
+
// res should have headers
|
|
336
|
+
handleResult(err, null);
|
|
337
|
+
});
|
|
338
|
+
if (data && typeof data === 'string') {
|
|
339
|
+
req.write(data, 'utf8');
|
|
340
|
+
}
|
|
341
|
+
if (data && typeof data !== 'string') {
|
|
342
|
+
data.on('close', function () {
|
|
343
|
+
req.end();
|
|
344
|
+
});
|
|
345
|
+
data.pipe(req);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
req.end();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Gets an http agent. This function is useful when you need an http agent that handles
|
|
353
|
+
* routing through a proxy server - depending upon the url and proxy environment variables.
|
|
354
|
+
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
|
|
355
|
+
*/
|
|
356
|
+
getAgent(serverUrl) {
|
|
357
|
+
let parsedUrl = new URL(serverUrl);
|
|
358
|
+
return this._getAgent(parsedUrl);
|
|
359
|
+
}
|
|
360
|
+
_prepareRequest(method, requestUrl, headers) {
|
|
361
|
+
const info = {};
|
|
362
|
+
info.parsedUrl = requestUrl;
|
|
363
|
+
const usingSsl = info.parsedUrl.protocol === 'https:';
|
|
364
|
+
info.httpModule = usingSsl ? https : http;
|
|
365
|
+
const defaultPort = usingSsl ? 443 : 80;
|
|
366
|
+
info.options = {};
|
|
367
|
+
info.options.host = info.parsedUrl.hostname;
|
|
368
|
+
info.options.port = info.parsedUrl.port
|
|
369
|
+
? parseInt(info.parsedUrl.port)
|
|
370
|
+
: defaultPort;
|
|
371
|
+
info.options.path =
|
|
372
|
+
(info.parsedUrl.pathname || '') + (info.parsedUrl.search || '');
|
|
373
|
+
info.options.method = method;
|
|
374
|
+
info.options.headers = this._mergeHeaders(headers);
|
|
375
|
+
if (this.userAgent != null) {
|
|
376
|
+
info.options.headers['user-agent'] = this.userAgent;
|
|
377
|
+
}
|
|
378
|
+
info.options.agent = this._getAgent(info.parsedUrl);
|
|
379
|
+
// gives handlers an opportunity to participate
|
|
380
|
+
if (this.handlers) {
|
|
381
|
+
this.handlers.forEach(handler => {
|
|
382
|
+
handler.prepareRequest(info.options);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return info;
|
|
386
|
+
}
|
|
387
|
+
_mergeHeaders(headers) {
|
|
388
|
+
const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
|
|
389
|
+
if (this.requestOptions && this.requestOptions.headers) {
|
|
390
|
+
return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers));
|
|
391
|
+
}
|
|
392
|
+
return lowercaseKeys(headers || {});
|
|
393
|
+
}
|
|
394
|
+
_getExistingOrDefaultHeader(additionalHeaders, header, _default) {
|
|
395
|
+
const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
|
|
396
|
+
let clientHeader;
|
|
397
|
+
if (this.requestOptions && this.requestOptions.headers) {
|
|
398
|
+
clientHeader = lowercaseKeys(this.requestOptions.headers)[header];
|
|
399
|
+
}
|
|
400
|
+
return additionalHeaders[header] || clientHeader || _default;
|
|
401
|
+
}
|
|
402
|
+
_getAgent(parsedUrl) {
|
|
403
|
+
let agent;
|
|
404
|
+
let proxyUrl = pm.getProxyUrl(parsedUrl);
|
|
405
|
+
let useProxy = proxyUrl && proxyUrl.hostname;
|
|
406
|
+
if (this._keepAlive && useProxy) {
|
|
407
|
+
agent = this._proxyAgent;
|
|
408
|
+
}
|
|
409
|
+
if (this._keepAlive && !useProxy) {
|
|
410
|
+
agent = this._agent;
|
|
411
|
+
}
|
|
412
|
+
// if agent is already assigned use that agent.
|
|
413
|
+
if (!!agent) {
|
|
414
|
+
return agent;
|
|
415
|
+
}
|
|
416
|
+
const usingSsl = parsedUrl.protocol === 'https:';
|
|
417
|
+
let maxSockets = 100;
|
|
418
|
+
if (!!this.requestOptions) {
|
|
419
|
+
maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets;
|
|
420
|
+
}
|
|
421
|
+
if (useProxy) {
|
|
422
|
+
// If using proxy, need tunnel
|
|
423
|
+
if (!tunnel) {
|
|
424
|
+
tunnel = require('tunnel');
|
|
425
|
+
}
|
|
426
|
+
const agentOptions = {
|
|
427
|
+
maxSockets: maxSockets,
|
|
428
|
+
keepAlive: this._keepAlive,
|
|
429
|
+
proxy: {
|
|
430
|
+
...((proxyUrl.username || proxyUrl.password) && {
|
|
431
|
+
proxyAuth: `${proxyUrl.username}:${proxyUrl.password}`
|
|
432
|
+
}),
|
|
433
|
+
host: proxyUrl.hostname,
|
|
434
|
+
port: proxyUrl.port
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
let tunnelAgent;
|
|
438
|
+
const overHttps = proxyUrl.protocol === 'https:';
|
|
439
|
+
if (usingSsl) {
|
|
440
|
+
tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp;
|
|
444
|
+
}
|
|
445
|
+
agent = tunnelAgent(agentOptions);
|
|
446
|
+
this._proxyAgent = agent;
|
|
447
|
+
}
|
|
448
|
+
// if reusing agent across request and tunneling agent isn't assigned create a new agent
|
|
449
|
+
if (this._keepAlive && !agent) {
|
|
450
|
+
const options = { keepAlive: this._keepAlive, maxSockets: maxSockets };
|
|
451
|
+
agent = usingSsl ? new https.Agent(options) : new http.Agent(options);
|
|
452
|
+
this._agent = agent;
|
|
453
|
+
}
|
|
454
|
+
// if not using private agent and tunnel agent isn't setup then use global agent
|
|
455
|
+
if (!agent) {
|
|
456
|
+
agent = usingSsl ? https.globalAgent : http.globalAgent;
|
|
457
|
+
}
|
|
458
|
+
if (usingSsl && this._ignoreSslError) {
|
|
459
|
+
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
|
|
460
|
+
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
|
|
461
|
+
// we have to cast it to any and change it directly
|
|
462
|
+
agent.options = Object.assign(agent.options || {}, {
|
|
463
|
+
rejectUnauthorized: false
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return agent;
|
|
467
|
+
}
|
|
468
|
+
_performExponentialBackoff(retryNumber) {
|
|
469
|
+
retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber);
|
|
470
|
+
const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber);
|
|
471
|
+
return new Promise(resolve => setTimeout(() => resolve(), ms));
|
|
472
|
+
}
|
|
473
|
+
static dateTimeDeserializer(key, value) {
|
|
474
|
+
if (typeof value === 'string') {
|
|
475
|
+
let a = new Date(value);
|
|
476
|
+
if (!isNaN(a.valueOf())) {
|
|
477
|
+
return a;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
async _processResponse(res, options) {
|
|
483
|
+
return new Promise(async (resolve, reject) => {
|
|
484
|
+
const statusCode = res.message.statusCode;
|
|
485
|
+
const response = {
|
|
486
|
+
statusCode: statusCode,
|
|
487
|
+
result: null,
|
|
488
|
+
headers: {}
|
|
489
|
+
};
|
|
490
|
+
// not found leads to null obj returned
|
|
491
|
+
if (statusCode == HttpCodes.NotFound) {
|
|
492
|
+
resolve(response);
|
|
493
|
+
}
|
|
494
|
+
let obj;
|
|
495
|
+
let contents;
|
|
496
|
+
// get the result from the body
|
|
497
|
+
try {
|
|
498
|
+
contents = await res.readBody();
|
|
499
|
+
if (contents && contents.length > 0) {
|
|
500
|
+
if (options && options.deserializeDates) {
|
|
501
|
+
obj = JSON.parse(contents, HttpClient.dateTimeDeserializer);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
obj = JSON.parse(contents);
|
|
505
|
+
}
|
|
506
|
+
response.result = obj;
|
|
507
|
+
}
|
|
508
|
+
response.headers = res.message.headers;
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
// Invalid resource (contents not json); leaving result obj null
|
|
512
|
+
}
|
|
513
|
+
// note that 3xx redirects are handled by the http layer.
|
|
514
|
+
if (statusCode > 299) {
|
|
515
|
+
let msg;
|
|
516
|
+
// if exception/error in body, attempt to get better error
|
|
517
|
+
if (obj && obj.message) {
|
|
518
|
+
msg = obj.message;
|
|
519
|
+
}
|
|
520
|
+
else if (contents && contents.length > 0) {
|
|
521
|
+
// it may be the case that the exception is in the body message as string
|
|
522
|
+
msg = contents;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
msg = 'Failed request: (' + statusCode + ')';
|
|
526
|
+
}
|
|
527
|
+
let err = new HttpClientError(msg, statusCode);
|
|
528
|
+
err.result = response.result;
|
|
529
|
+
reject(err);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
resolve(response);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
exports.HttpClient = HttpClient;
|
package/interfaces.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import http = require('http');
|
|
3
|
+
export interface IHeaders {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
export interface IHttpClient {
|
|
7
|
+
options(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
8
|
+
get(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
9
|
+
del(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
10
|
+
post(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
11
|
+
patch(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
12
|
+
put(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
13
|
+
sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
|
14
|
+
request(verb: string, requestUrl: string, data: string | NodeJS.ReadableStream, headers: IHeaders): Promise<IHttpClientResponse>;
|
|
15
|
+
requestRaw(info: IRequestInfo, data: string | NodeJS.ReadableStream): Promise<IHttpClientResponse>;
|
|
16
|
+
requestRawWithCallback(info: IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: IHttpClientResponse) => void): void;
|
|
17
|
+
}
|
|
18
|
+
export interface IRequestHandler {
|
|
19
|
+
prepareRequest(options: http.RequestOptions): void;
|
|
20
|
+
canHandleAuthentication(response: IHttpClientResponse): boolean;
|
|
21
|
+
handleAuthentication(httpClient: IHttpClient, requestInfo: IRequestInfo, objs: any): Promise<IHttpClientResponse>;
|
|
22
|
+
}
|
|
23
|
+
export interface IHttpClientResponse {
|
|
24
|
+
message: http.IncomingMessage;
|
|
25
|
+
readBody(): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
export interface IRequestInfo {
|
|
28
|
+
options: http.RequestOptions;
|
|
29
|
+
parsedUrl: URL;
|
|
30
|
+
httpModule: any;
|
|
31
|
+
}
|
|
32
|
+
export interface IRequestOptions {
|
|
33
|
+
headers?: IHeaders;
|
|
34
|
+
socketTimeout?: number;
|
|
35
|
+
ignoreSslError?: boolean;
|
|
36
|
+
allowRedirects?: boolean;
|
|
37
|
+
allowRedirectDowngrade?: boolean;
|
|
38
|
+
maxRedirects?: number;
|
|
39
|
+
maxSockets?: number;
|
|
40
|
+
keepAlive?: boolean;
|
|
41
|
+
deserializeDates?: boolean;
|
|
42
|
+
allowRetries?: boolean;
|
|
43
|
+
maxRetries?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ITypedResponse<T> {
|
|
46
|
+
statusCode: number;
|
|
47
|
+
result: T | null;
|
|
48
|
+
headers: Object;
|
|
49
|
+
}
|
package/interfaces.js
ADDED
package/package.json
CHANGED
package/proxy.d.ts
ADDED
package/proxy.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function getProxyUrl(reqUrl) {
|
|
4
|
+
let usingSsl = reqUrl.protocol === 'https:';
|
|
5
|
+
let proxyUrl;
|
|
6
|
+
if (checkBypass(reqUrl)) {
|
|
7
|
+
return proxyUrl;
|
|
8
|
+
}
|
|
9
|
+
let proxyVar;
|
|
10
|
+
if (usingSsl) {
|
|
11
|
+
proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY'];
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY'];
|
|
15
|
+
}
|
|
16
|
+
if (proxyVar) {
|
|
17
|
+
proxyUrl = new URL(proxyVar);
|
|
18
|
+
}
|
|
19
|
+
return proxyUrl;
|
|
20
|
+
}
|
|
21
|
+
exports.getProxyUrl = getProxyUrl;
|
|
22
|
+
function checkBypass(reqUrl) {
|
|
23
|
+
if (!reqUrl.hostname) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
|
|
27
|
+
if (!noProxy) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Determine the request port
|
|
31
|
+
let reqPort;
|
|
32
|
+
if (reqUrl.port) {
|
|
33
|
+
reqPort = Number(reqUrl.port);
|
|
34
|
+
}
|
|
35
|
+
else if (reqUrl.protocol === 'http:') {
|
|
36
|
+
reqPort = 80;
|
|
37
|
+
}
|
|
38
|
+
else if (reqUrl.protocol === 'https:') {
|
|
39
|
+
reqPort = 443;
|
|
40
|
+
}
|
|
41
|
+
// Format the request hostname and hostname with port
|
|
42
|
+
let upperReqHosts = [reqUrl.hostname.toUpperCase()];
|
|
43
|
+
if (typeof reqPort === 'number') {
|
|
44
|
+
upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`);
|
|
45
|
+
}
|
|
46
|
+
// Compare request host against noproxy
|
|
47
|
+
for (let upperNoProxyItem of noProxy
|
|
48
|
+
.split(',')
|
|
49
|
+
.map(x => x.trim().toUpperCase())
|
|
50
|
+
.filter(x => x)) {
|
|
51
|
+
if (upperReqHosts.some(x => x === upperNoProxyItem)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
exports.checkBypass = checkBypass;
|