Merge branch 'main' into dhadka/typed-error

This commit is contained in:
David Hadka
2020-10-12 10:58:50 -05:00
committed by GitHub
7 changed files with 95 additions and 41 deletions

View File

@@ -2,7 +2,7 @@ name: http-tests
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '**.md' - '**.md'
pull_request: pull_request:

View File

@@ -3,6 +3,9 @@
## 1.0.9 ## 1.0.9
Throw HttpClientError instead of a generic Error from the \<verb>Json() helper methods when the server responds with a non-successful status code. Throw HttpClientError instead of a generic Error from the \<verb>Json() helper methods when the server responds with a non-successful status code.
## 1.0.8
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)
## 1.0.7 ## 1.0.7
Update NPM dependencies and add 429 to the list of HttpCodes Update NPM dependencies and add 429 to the list of HttpCodes
@@ -16,4 +19,4 @@ Adds \<verb>Json() helper methods for json over http scenarios.
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. 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.
## 1.0.1 to 1.0.3 ## 1.0.1 to 1.0.3
Adds proxy support. Adds proxy support.

View File

@@ -128,7 +128,7 @@ describe('basics', () => {
}) })
}) })
it('does basic get request with redirects', async done => { it.skip('does basic get request with redirects', async done => {
let res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' + 'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/get') encodeURIComponent('https://httpbin.org/get')
@@ -140,7 +140,7 @@ describe('basics', () => {
done() done()
}) })
it('does basic get request with redirects (303)', async done => { it.skip('does basic get request with redirects (303)', async done => {
let res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' + 'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/get') + encodeURIComponent('https://httpbin.org/get') +
@@ -164,7 +164,7 @@ describe('basics', () => {
done() done()
}) })
it('does not follow redirects if disabled', async done => { it.skip('does not follow redirects if disabled', async done => {
let http: httpm.HttpClient = new httpm.HttpClient( let http: httpm.HttpClient = new httpm.HttpClient(
'typed-test-client-tests', 'typed-test-client-tests',
null, null,
@@ -179,6 +179,52 @@ describe('basics', () => {
done() done()
}) })
it.skip('does not pass auth with diff hostname redirects', async done => {
let headers = {
accept: 'application/json',
authorization: 'shhh'
}
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://www.httpbin.org/get'),
headers
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
// httpbin "fixes" the casing
expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.httpbin.org/get')
done()
})
it.skip('does not pass Auth with diff hostname redirects', async done => {
let headers = {
Accept: 'application/json',
Authorization: 'shhh'
}
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://www.httpbin.org/get'),
headers
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
// httpbin "fixes" the casing
expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.httpbin.org/get')
done()
})
it('does basic head request', async done => { it('does basic head request', async done => {
let res: httpm.HttpClientResponse = await _http.head( let res: httpm.HttpClientResponse = await _http.head(
'http://httpbin.org/get' 'http://httpbin.org/get'

View File

@@ -2,7 +2,6 @@ import * as http from 'http'
import * as httpm from '../_out' import * as httpm from '../_out'
import * as pm from '../_out/proxy' import * as pm from '../_out/proxy'
import * as proxy from 'proxy' import * as proxy from 'proxy'
import * as url from 'url'
let _proxyConnects: string[] let _proxyConnects: string[]
let _proxyServer: http.Server let _proxyServer: http.Server
@@ -39,107 +38,107 @@ describe('proxy', () => {
}) })
it('getProxyUrl does not return proxyUrl if variables not set', () => { it('getProxyUrl does not return proxyUrl if variables not set', () => {
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => { it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => { it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => {
process.env['http_proxy'] = 'https://myproxysvr' process.env['http_proxy'] = 'https://myproxysvr'
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => { it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
let proxyUrl = pm.getProxyUrl(url.parse('http://github.com')) let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => { it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(url.parse('https://myserver')) let proxyUrl = pm.getProxyUrl(new URL('https://myserver'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => { it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => { it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(url.parse('http://myserver')) let proxyUrl = pm.getProxyUrl(new URL('http://myserver'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => { it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(url.parse('http://github.com')) let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('checkBypass returns true if host as no_proxy list', () => { it('checkBypass returns true if host as no_proxy list', () => {
process.env['no_proxy'] = 'myserver' process.env['no_proxy'] = 'myserver'
let bypass = pm.checkBypass(url.parse('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list', () => { it('checkBypass returns true if host in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let bypass = pm.checkBypass(url.parse('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with spaces', () => { it('checkBypass returns true if host in no_proxy list with spaces', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
let bypass = pm.checkBypass(url.parse('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with port', () => { it('checkBypass returns true if host in no_proxy list with port', () => {
process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver'
let bypass = pm.checkBypass(url.parse('https://myserver:8080')) let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host with port in no_proxy list without port', () => { it('checkBypass returns true if host with port in no_proxy list without port', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver'
let bypass = pm.checkBypass(url.parse('https://myserver:8080')) let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with default https port', () => { it('checkBypass returns true if host in no_proxy list with default https port', () => {
process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver'
let bypass = pm.checkBypass(url.parse('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with default http port', () => { it('checkBypass returns true if host in no_proxy list with default http port', () => {
process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver'
let bypass = pm.checkBypass(url.parse('http://myserver')) let bypass = pm.checkBypass(new URL('http://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns false if host not in no_proxy list', () => { it('checkBypass returns false if host not in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
let bypass = pm.checkBypass(url.parse('https://github.com')) let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy() expect(bypass).toBeFalsy()
}) })
it('checkBypass returns false if empty no_proxy', () => { it('checkBypass returns false if empty no_proxy', () => {
process.env['no_proxy'] = '' process.env['no_proxy'] = ''
let bypass = pm.checkBypass(url.parse('https://github.com')) let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy() expect(bypass).toBeFalsy()
}) })

View File

@@ -1,4 +1,3 @@
import url = require('url')
import http = require('http') import http = require('http')
import https = require('https') import https = require('https')
import ifm = require('./interfaces') import ifm = require('./interfaces')
@@ -50,7 +49,7 @@ export enum MediaTypes {
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/ */
export function getProxyUrl(serverUrl: string): string { export function getProxyUrl(serverUrl: string): string {
let proxyUrl = pm.getProxyUrl(url.parse(serverUrl)) let proxyUrl = pm.getProxyUrl(new URL(serverUrl))
return proxyUrl ? proxyUrl.href : '' return proxyUrl ? proxyUrl.href : ''
} }
@@ -104,7 +103,7 @@ export class HttpClientResponse implements ifm.IHttpClientResponse {
} }
export function isHttps(requestUrl: string) { export function isHttps(requestUrl: string) {
let parsedUrl: url.Url = url.parse(requestUrl) let parsedUrl: URL = new URL(requestUrl)
return parsedUrl.protocol === 'https:' return parsedUrl.protocol === 'https:'
} }
@@ -334,7 +333,7 @@ export class HttpClient {
throw new Error('Client has already been disposed.') throw new Error('Client has already been disposed.')
} }
let parsedUrl = url.parse(requestUrl) let parsedUrl = new URL(requestUrl)
let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers) let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers)
// Only perform retries on reads since writes may not be idempotent. // Only perform retries on reads since writes may not be idempotent.
@@ -383,7 +382,7 @@ export class HttpClient {
// if there's no location to redirect to, we won't // if there's no location to redirect to, we won't
break break
} }
let parsedRedirectUrl = url.parse(redirectUrl) let parsedRedirectUrl = new URL(redirectUrl)
if ( if (
parsedUrl.protocol == 'https:' && parsedUrl.protocol == 'https:' &&
parsedUrl.protocol != parsedRedirectUrl.protocol && parsedUrl.protocol != parsedRedirectUrl.protocol &&
@@ -398,6 +397,16 @@ export class HttpClient {
// which will leak the open socket. // which will leak the open socket.
await response.readBody() await response.readBody()
// strip authorization header if redirected to a different hostname
if (parsedRedirectUrl.hostname !== parsedUrl.hostname) {
for (let header in headers) {
// header names are case insensitive
if (header.toLowerCase() === 'authorization') {
delete headers[header]
}
}
}
// let's make the request with the new redirectUrl // let's make the request with the new redirectUrl
info = this._prepareRequest(verb, parsedRedirectUrl, headers) info = this._prepareRequest(verb, parsedRedirectUrl, headers)
response = await this.requestRaw(info, data) response = await this.requestRaw(info, data)
@@ -528,13 +537,13 @@ export class HttpClient {
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/ */
public getAgent(serverUrl: string): http.Agent { public getAgent(serverUrl: string): http.Agent {
let parsedUrl = url.parse(serverUrl) let parsedUrl = new URL(serverUrl)
return this._getAgent(parsedUrl) return this._getAgent(parsedUrl)
} }
private _prepareRequest( private _prepareRequest(
method: string, method: string,
requestUrl: url.Url, requestUrl: URL,
headers: ifm.IHeaders headers: ifm.IHeaders
): ifm.IRequestInfo { ): ifm.IRequestInfo {
const info: ifm.IRequestInfo = <ifm.IRequestInfo>{} const info: ifm.IRequestInfo = <ifm.IRequestInfo>{}
@@ -599,9 +608,9 @@ export class HttpClient {
return additionalHeaders[header] || clientHeader || _default return additionalHeaders[header] || clientHeader || _default
} }
private _getAgent(parsedUrl: url.Url): http.Agent { private _getAgent(parsedUrl: URL): http.Agent {
let agent let agent
let proxyUrl: url.Url = pm.getProxyUrl(parsedUrl) let proxyUrl: URL = pm.getProxyUrl(parsedUrl)
let useProxy = proxyUrl && proxyUrl.hostname let useProxy = proxyUrl && proxyUrl.hostname
if (this._keepAlive && useProxy) { if (this._keepAlive && useProxy) {
@@ -633,7 +642,7 @@ export class HttpClient {
maxSockets: maxSockets, maxSockets: maxSockets,
keepAlive: this._keepAlive, keepAlive: this._keepAlive,
proxy: { proxy: {
proxyAuth: proxyUrl.auth, proxyAuth: `${proxyUrl.username}:${proxyUrl.password}`,
host: proxyUrl.hostname, host: proxyUrl.hostname,
port: proxyUrl.port port: proxyUrl.port
} }

View File

@@ -1,5 +1,4 @@
import http = require('http') import http = require('http')
import url = require('url')
export interface IHeaders { export interface IHeaders {
[key: string]: any [key: string]: any
@@ -73,7 +72,7 @@ export interface IHttpClientResponse {
export interface IRequestInfo { export interface IRequestInfo {
options: http.RequestOptions options: http.RequestOptions
parsedUrl: url.Url parsedUrl: URL
httpModule: any httpModule: any
} }

View File

@@ -1,9 +1,7 @@
import * as url from 'url' export function getProxyUrl(reqUrl: URL): URL | undefined {
export function getProxyUrl(reqUrl: url.Url): url.Url | undefined {
let usingSsl = reqUrl.protocol === 'https:' let usingSsl = reqUrl.protocol === 'https:'
let proxyUrl: url.Url let proxyUrl: URL
if (checkBypass(reqUrl)) { if (checkBypass(reqUrl)) {
return proxyUrl return proxyUrl
} }
@@ -16,13 +14,13 @@ export function getProxyUrl(reqUrl: url.Url): url.Url | undefined {
} }
if (proxyVar) { if (proxyVar) {
proxyUrl = url.parse(proxyVar) proxyUrl = new URL(proxyVar)
} }
return proxyUrl return proxyUrl
} }
export function checkBypass(reqUrl: url.Url): boolean { export function checkBypass(reqUrl: URL): boolean {
if (!reqUrl.hostname) { if (!reqUrl.hostname) {
return false return false
} }