@actions/http-client 1.0.7 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Releases
2
2
 
3
+ ## 1.0.10
4
+
5
+ Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42)
6
+
7
+ ## 1.0.9
8
+ Throw HttpClientError instead of a generic Error from the \<verb>Json() helper methods when the server responds with a non-successful status code.
9
+
10
+ ## 1.0.8
11
+ Fixed security issue where a redirect (e.g. 302) to another domain would pass headers. The fix was to strip the authorization header if the hostname was different. More [details in PR #27](https://github.com/actions/http-client/pull/27)
12
+
3
13
  ## 1.0.7
4
14
  Update NPM dependencies and add 429 to the list of HttpCodes
5
15
 
@@ -13,4 +23,4 @@ Adds \<verb>Json() helper methods for json over http scenarios.
13
23
  Started to add \<verb>Json() helper methods. Do not use this release for that. Use >= 1.0.5 since there was an issue with types.
14
24
 
15
25
  ## 1.0.1 to 1.0.3
16
- Adds proxy support.
26
+ Adds proxy support.
package/auth.js CHANGED
@@ -6,7 +6,9 @@ class BasicCredentialHandler {
6
6
  this.password = password;
7
7
  }
8
8
  prepareRequest(options) {
9
- options.headers['Authorization'] = 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64');
9
+ options.headers['Authorization'] =
10
+ 'Basic ' +
11
+ Buffer.from(this.username + ':' + this.password).toString('base64');
10
12
  }
11
13
  // This handler cannot handle 401
12
14
  canHandleAuthentication(response) {
@@ -42,7 +44,8 @@ class PersonalAccessTokenCredentialHandler {
42
44
  // currently implements pre-authorization
43
45
  // TODO: support preAuth = false where it hooks on 401
44
46
  prepareRequest(options) {
45
- options.headers['Authorization'] = 'Basic ' + Buffer.from('PAT:' + this.token).toString('base64');
47
+ options.headers['Authorization'] =
48
+ 'Basic ' + Buffer.from('PAT:' + this.token).toString('base64');
46
49
  }
47
50
  // This handler cannot handle 401
48
51
  canHandleAuthentication(response) {
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import http = require("http");
2
+ import http = require('http');
3
3
  import ifm = require('./interfaces');
4
4
  export declare enum HttpCodes {
5
5
  OK = 200,
@@ -42,6 +42,11 @@ export declare enum MediaTypes {
42
42
  * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
43
43
  */
44
44
  export declare function getProxyUrl(serverUrl: string): string;
45
+ export declare class HttpClientError extends Error {
46
+ constructor(message: string, statusCode: number);
47
+ statusCode: number;
48
+ result?: any;
49
+ }
45
50
  export declare class HttpClientResponse implements ifm.IHttpClientResponse {
46
51
  constructor(message: http.IncomingMessage);
47
52
  message: http.IncomingMessage;
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const url = require("url");
4
3
  const http = require("http");
5
4
  const https = require("https");
6
5
  const pm = require("./proxy");
@@ -49,15 +48,34 @@ var MediaTypes;
49
48
  * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
50
49
  */
51
50
  function getProxyUrl(serverUrl) {
52
- let proxyUrl = pm.getProxyUrl(url.parse(serverUrl));
51
+ let proxyUrl = pm.getProxyUrl(new URL(serverUrl));
53
52
  return proxyUrl ? proxyUrl.href : '';
54
53
  }
55
54
  exports.getProxyUrl = getProxyUrl;
56
- const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect];
57
- const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout];
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
+ ];
58
67
  const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD'];
59
68
  const ExponentialBackoffCeiling = 10;
60
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;
61
79
  class HttpClientResponse {
62
80
  constructor(message) {
63
81
  this.message = message;
@@ -76,7 +94,7 @@ class HttpClientResponse {
76
94
  }
77
95
  exports.HttpClientResponse = HttpClientResponse;
78
96
  function isHttps(requestUrl) {
79
- let parsedUrl = url.parse(requestUrl);
97
+ let parsedUrl = new URL(requestUrl);
80
98
  return parsedUrl.protocol === 'https:';
81
99
  }
82
100
  exports.isHttps = isHttps;
@@ -179,18 +197,22 @@ class HttpClient {
179
197
  */
180
198
  async request(verb, requestUrl, data, headers) {
181
199
  if (this._disposed) {
182
- throw new Error("Client has already been disposed.");
200
+ throw new Error('Client has already been disposed.');
183
201
  }
184
- let parsedUrl = url.parse(requestUrl);
202
+ let parsedUrl = new URL(requestUrl);
185
203
  let info = this._prepareRequest(verb, parsedUrl, headers);
186
204
  // Only perform retries on reads since writes may not be idempotent.
187
- let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1;
205
+ let maxTries = this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
206
+ ? this._maxRetries + 1
207
+ : 1;
188
208
  let numTries = 0;
189
209
  let response;
190
210
  while (numTries < maxTries) {
191
211
  response = await this.requestRaw(info, data);
192
212
  // Check if it's an authentication challenge
193
- if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
213
+ if (response &&
214
+ response.message &&
215
+ response.message.statusCode === HttpCodes.Unauthorized) {
194
216
  let authenticationHandler;
195
217
  for (let i = 0; i < this.handlers.length; i++) {
196
218
  if (this.handlers[i].canHandleAuthentication(response)) {
@@ -208,21 +230,32 @@ class HttpClient {
208
230
  }
209
231
  }
210
232
  let redirectsRemaining = this._maxRedirects;
211
- while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1
212
- && this._allowRedirects
213
- && redirectsRemaining > 0) {
214
- const redirectUrl = response.message.headers["location"];
233
+ while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
234
+ this._allowRedirects &&
235
+ redirectsRemaining > 0) {
236
+ const redirectUrl = response.message.headers['location'];
215
237
  if (!redirectUrl) {
216
238
  // if there's no location to redirect to, we won't
217
239
  break;
218
240
  }
219
- let parsedRedirectUrl = url.parse(redirectUrl);
220
- if (parsedUrl.protocol == 'https:' && parsedUrl.protocol != parsedRedirectUrl.protocol && !this._allowRedirectDowngrade) {
221
- 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.");
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.');
222
246
  }
223
247
  // we need to finish reading the response before reassigning response
224
248
  // which will leak the open socket.
225
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
+ }
226
259
  // let's make the request with the new redirectUrl
227
260
  info = this._prepareRequest(verb, parsedRedirectUrl, headers);
228
261
  response = await this.requestRaw(info, data);
@@ -273,8 +306,8 @@ class HttpClient {
273
306
  */
274
307
  requestRawWithCallback(info, data, onResult) {
275
308
  let socket;
276
- if (typeof (data) === 'string') {
277
- info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8');
309
+ if (typeof data === 'string') {
310
+ info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
278
311
  }
279
312
  let callbackCalled = false;
280
313
  let handleResult = (err, res) => {
@@ -287,7 +320,7 @@ class HttpClient {
287
320
  let res = new HttpClientResponse(msg);
288
321
  handleResult(null, res);
289
322
  });
290
- req.on('socket', (sock) => {
323
+ req.on('socket', sock => {
291
324
  socket = sock;
292
325
  });
293
326
  // If we ever get disconnected, we want the socket to timeout eventually
@@ -302,10 +335,10 @@ class HttpClient {
302
335
  // res should have headers
303
336
  handleResult(err, null);
304
337
  });
305
- if (data && typeof (data) === 'string') {
338
+ if (data && typeof data === 'string') {
306
339
  req.write(data, 'utf8');
307
340
  }
308
- if (data && typeof (data) !== 'string') {
341
+ if (data && typeof data !== 'string') {
309
342
  data.on('close', function () {
310
343
  req.end();
311
344
  });
@@ -321,7 +354,7 @@ class HttpClient {
321
354
  * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
322
355
  */
323
356
  getAgent(serverUrl) {
324
- let parsedUrl = url.parse(serverUrl);
357
+ let parsedUrl = new URL(serverUrl);
325
358
  return this._getAgent(parsedUrl);
326
359
  }
327
360
  _prepareRequest(method, requestUrl, headers) {
@@ -332,31 +365,34 @@ class HttpClient {
332
365
  const defaultPort = usingSsl ? 443 : 80;
333
366
  info.options = {};
334
367
  info.options.host = info.parsedUrl.hostname;
335
- info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort;
336
- info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || '');
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 || '');
337
373
  info.options.method = method;
338
374
  info.options.headers = this._mergeHeaders(headers);
339
375
  if (this.userAgent != null) {
340
- info.options.headers["user-agent"] = this.userAgent;
376
+ info.options.headers['user-agent'] = this.userAgent;
341
377
  }
342
378
  info.options.agent = this._getAgent(info.parsedUrl);
343
379
  // gives handlers an opportunity to participate
344
380
  if (this.handlers) {
345
- this.handlers.forEach((handler) => {
381
+ this.handlers.forEach(handler => {
346
382
  handler.prepareRequest(info.options);
347
383
  });
348
384
  }
349
385
  return info;
350
386
  }
351
387
  _mergeHeaders(headers) {
352
- const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {});
388
+ const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
353
389
  if (this.requestOptions && this.requestOptions.headers) {
354
390
  return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers));
355
391
  }
356
392
  return lowercaseKeys(headers || {});
357
393
  }
358
394
  _getExistingOrDefaultHeader(additionalHeaders, header, _default) {
359
- const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {});
395
+ const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
360
396
  let clientHeader;
361
397
  if (this.requestOptions && this.requestOptions.headers) {
362
398
  clientHeader = lowercaseKeys(this.requestOptions.headers)[header];
@@ -391,10 +427,12 @@ class HttpClient {
391
427
  maxSockets: maxSockets,
392
428
  keepAlive: this._keepAlive,
393
429
  proxy: {
394
- proxyAuth: proxyUrl.auth,
430
+ ...((proxyUrl.username || proxyUrl.password) && {
431
+ proxyAuth: `${proxyUrl.username}:${proxyUrl.password}`
432
+ }),
395
433
  host: proxyUrl.hostname,
396
434
  port: proxyUrl.port
397
- },
435
+ }
398
436
  };
399
437
  let tunnelAgent;
400
438
  const overHttps = proxyUrl.protocol === 'https:';
@@ -421,7 +459,9 @@ class HttpClient {
421
459
  // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
422
460
  // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
423
461
  // we have to cast it to any and change it directly
424
- agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false });
462
+ agent.options = Object.assign(agent.options || {}, {
463
+ rejectUnauthorized: false
464
+ });
425
465
  }
426
466
  return agent;
427
467
  }
@@ -482,14 +522,10 @@ class HttpClient {
482
522
  msg = contents;
483
523
  }
484
524
  else {
485
- msg = "Failed request: (" + statusCode + ")";
486
- }
487
- let err = new Error(msg);
488
- // attach statusCode and body obj (if available) to the error object
489
- err['statusCode'] = statusCode;
490
- if (response.result) {
491
- err['result'] = response.result;
525
+ msg = 'Failed request: (' + statusCode + ')';
492
526
  }
527
+ let err = new HttpClientError(msg, statusCode);
528
+ err.result = response.result;
493
529
  reject(err);
494
530
  }
495
531
  else {
package/interfaces.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /// <reference types="node" />
2
- import http = require("http");
3
- import url = require("url");
2
+ import http = require('http');
4
3
  export interface IHeaders {
5
4
  [key: string]: any;
6
5
  }
@@ -27,7 +26,7 @@ export interface IHttpClientResponse {
27
26
  }
28
27
  export interface IRequestInfo {
29
28
  options: http.RequestOptions;
30
- parsedUrl: url.Url;
29
+ parsedUrl: URL;
31
30
  httpModule: any;
32
31
  }
33
32
  export interface IRequestOptions {
package/interfaces.js CHANGED
@@ -1,3 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- ;
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@actions/http-client",
3
- "version": "1.0.7",
3
+ "version": "1.0.11",
4
4
  "description": "Actions Http Client",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out",
8
- "test": "jest"
8
+ "test": "jest",
9
+ "format": "prettier --write *.ts && prettier --write **/*.ts",
10
+ "format-check": "prettier --check *.ts && prettier --check **/*.ts",
11
+ "audit-check": "npm audit --audit-level=moderate"
9
12
  },
10
13
  "repository": {
11
14
  "type": "git",
@@ -25,6 +28,7 @@
25
28
  "@types/jest": "^25.1.4",
26
29
  "@types/node": "^12.12.31",
27
30
  "jest": "^25.1.0",
31
+ "prettier": "^2.0.4",
28
32
  "proxy": "^1.0.1",
29
33
  "ts-jest": "^25.2.1",
30
34
  "typescript": "^3.8.3"
package/proxy.d.ts CHANGED
@@ -1,4 +1,2 @@
1
- /// <reference types="node" />
2
- import * as url from 'url';
3
- export declare function getProxyUrl(reqUrl: url.Url): url.Url | undefined;
4
- export declare function checkBypass(reqUrl: url.Url): boolean;
1
+ export declare function getProxyUrl(reqUrl: URL): URL | undefined;
2
+ export declare function checkBypass(reqUrl: URL): boolean;
package/proxy.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const url = require("url");
4
3
  function getProxyUrl(reqUrl) {
5
4
  let usingSsl = reqUrl.protocol === 'https:';
6
5
  let proxyUrl;
@@ -9,15 +8,13 @@ function getProxyUrl(reqUrl) {
9
8
  }
10
9
  let proxyVar;
11
10
  if (usingSsl) {
12
- proxyVar = process.env["https_proxy"] ||
13
- process.env["HTTPS_PROXY"];
11
+ proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY'];
14
12
  }
15
13
  else {
16
- proxyVar = process.env["http_proxy"] ||
17
- process.env["HTTP_PROXY"];
14
+ proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY'];
18
15
  }
19
16
  if (proxyVar) {
20
- proxyUrl = url.parse(proxyVar);
17
+ proxyUrl = new URL(proxyVar);
21
18
  }
22
19
  return proxyUrl;
23
20
  }
@@ -26,7 +23,7 @@ function checkBypass(reqUrl) {
26
23
  if (!reqUrl.hostname) {
27
24
  return false;
28
25
  }
29
- let noProxy = process.env["no_proxy"] || process.env["NO_PROXY"] || '';
26
+ let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
30
27
  if (!noProxy) {
31
28
  return false;
32
29
  }
@@ -47,7 +44,10 @@ function checkBypass(reqUrl) {
47
44
  upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`);
48
45
  }
49
46
  // Compare request host against noproxy
50
- for (let upperNoProxyItem of noProxy.split(',').map(x => x.trim().toUpperCase()).filter(x => x)) {
47
+ for (let upperNoProxyItem of noProxy
48
+ .split(',')
49
+ .map(x => x.trim().toUpperCase())
50
+ .filter(x => x)) {
51
51
  if (upperReqHosts.some(x => x === upperNoProxyItem)) {
52
52
  return true;
53
53
  }