1
0
Fork 0

Copy over http-client files

brcrista/http-client-diff
Brian Cristante 2022-04-28 09:33:03 -04:00
parent 3e2837ddce
commit bcb0e62b16
15 changed files with 12534 additions and 0 deletions

5
packages/http-client/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
_out
node_modules
.DS_Store
testoutput.txt
npm-debug.log

View File

@ -0,0 +1,21 @@
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,79 @@
<p align="center">
<img src="actions.png">
</p>
# Actions Http-Client
[![Http Status](https://github.com/actions/http-client/workflows/http-tests/badge.svg)](https://github.com/actions/http-client/actions)
A lightweight HTTP client optimized for use with actions, TypeScript with generics and async await.
## Features
- HTTP client with TypeScript generics and async/await/Promises
- Typings included so no need to acquire separately (great for intellisense and no versioning drift)
- [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner
- Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+.
- Basic, Bearer and PAT Support out of the box. Extensible handlers for others.
- Redirects supported
Features and releases [here](./RELEASES.md)
## Install
```
npm install @actions/http-client --save
```
## Samples
See the [HTTP](./__tests__) tests for detailed examples.
## Errors
### HTTP
The HTTP client does not throw unless truly exceptional.
* A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body.
* Redirects (3xx) will be followed by default.
See [HTTP tests](./__tests__) for detailed examples.
## Debugging
To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible:
```
export NODE_DEBUG=http
```
## Node support
The http-client is built using the latest LTS version of Node 12. It may work on previous node LTS versions but it's tested and officially supported on Node12+.
## Support and Versioning
We follow semver and will hold compatibility between major versions and increment the minor version with new features and capabilities (while holding compat).
## Contributing
We welcome PRs. Please create an issue and if applicable, a design before proceeding with code.
once:
```bash
$ npm install
```
To build:
```bash
$ npm run build
```
To run all tests:
```bash
$ npm test
```

View File

@ -0,0 +1,26 @@
## Releases
## 1.0.11
Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42)
## 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.
## 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
Update NPM dependencies and add 429 to the list of HttpCodes
## 1.0.6
Automatically sends Content-Type and Accept application/json headers for \<verb>Json() helper methods if not set in the client or parameters.
## 1.0.5
Adds \<verb>Json() helper methods for json over http scenarios.
## 1.0.4
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
Adds proxy support.

View File

@ -0,0 +1,61 @@
import * as httpm from '../_out'
import * as am from '../_out/auth'
describe('auth', () => {
beforeEach(() => {})
afterEach(() => {})
it('does basic http get request with basic auth', async () => {
let bh: am.BasicCredentialHandler = new am.BasicCredentialHandler(
'johndoe',
'password'
)
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [bh])
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
let auth: string = obj.headers.Authorization
let creds: string = Buffer.from(
auth.substring('Basic '.length),
'base64'
).toString()
expect(creds).toBe('johndoe:password')
expect(obj.url).toBe('http://httpbin.org/get')
})
it('does basic http get request with pat token auth', async () => {
let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
let ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler(
token
)
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
let auth: string = obj.headers.Authorization
let creds: string = Buffer.from(
auth.substring('Basic '.length),
'base64'
).toString()
expect(creds).toBe('PAT:' + token)
expect(obj.url).toBe('http://httpbin.org/get')
})
it('does basic http get request with pat token auth', async () => {
let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
let ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token)
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
let auth: string = obj.headers.Authorization
expect(auth).toBe('Bearer ' + token)
expect(obj.url).toBe('http://httpbin.org/get')
})
})

View File

@ -0,0 +1,375 @@
import * as httpm from '../_out'
import * as ifm from '../_out/interfaces'
import * as path from 'path'
import * as fs from 'fs'
let sampleFilePath: string = path.join(__dirname, 'testoutput.txt')
interface HttpBinData {
url: string
data: any
json: any
headers: any
args?: any
}
describe('basics', () => {
let _http: httpm.HttpClient
beforeEach(() => {
_http = new httpm.HttpClient('http-client-tests')
})
afterEach(() => {})
it('constructs', () => {
let http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests')
expect(http).toBeDefined()
})
// responses from httpbin return something like:
// {
// "args": {},
// "headers": {
// "Connection": "close",
// "Host": "httpbin.org",
// "User-Agent": "typed-test-client-tests"
// },
// "origin": "173.95.152.44",
// "url": "https://httpbin.org/get"
// }
it('does basic http get request', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['User-Agent']).toBeTruthy()
done()
})
it('does basic http get request with no user agent', async done => {
let http: httpm.HttpClient = new httpm.HttpClient()
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['User-Agent']).toBeFalsy()
done()
})
it('does basic https get request', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
done()
})
it('does basic http get request with default headers', async done => {
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['Content-Type']).toBe('application/json')
expect(obj.url).toBe('http://httpbin.org/get')
done()
})
it('does basic http get request with merged headers', async done => {
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
let res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get',
{
'content-type': 'application/x-www-form-urlencoded'
}
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['Content-Type']).toBe(
'application/x-www-form-urlencoded'
)
expect(obj.url).toBe('http://httpbin.org/get')
done()
})
it('pipes a get request', () => {
return new Promise<string>(async (resolve, reject) => {
let file: NodeJS.WritableStream = fs.createWriteStream(sampleFilePath)
;(await _http.get('https://httpbin.org/get')).message
.pipe(file)
.on('close', () => {
let body: string = fs.readFileSync(sampleFilePath).toString()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
resolve()
})
})
})
it('does basic get request with redirects', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/get')
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
done()
})
it('does basic get request with redirects (303)', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/get') +
'&status_code=303'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
done()
})
it('returns 404 for not found get request on redirect', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/status/404') +
'&status_code=303'
)
expect(res.message.statusCode).toBe(404)
let body: string = await res.readBody()
done()
})
it('does not follow redirects if disabled', async done => {
let http: httpm.HttpClient = new httpm.HttpClient(
'typed-test-client-tests',
null,
{allowRedirects: false}
)
let res: httpm.HttpClientResponse = await http.get(
'https://httpbin.org/redirect-to?url=' +
encodeURIComponent('https://httpbin.org/get')
)
expect(res.message.statusCode).toBe(302)
let body: string = await res.readBody()
done()
})
it('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 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 => {
let res: httpm.HttpClientResponse = await _http.head(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
done()
})
it('does basic http delete request', async done => {
let res: httpm.HttpClientResponse = await _http.del(
'http://httpbin.org/delete'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
done()
})
it('does basic http post request', async done => {
let b: string = 'Hello World!'
let res: httpm.HttpClientResponse = await _http.post(
'http://httpbin.org/post',
b
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/post')
done()
})
it('does basic http patch request', async done => {
let b: string = 'Hello World!'
let res: httpm.HttpClientResponse = await _http.patch(
'http://httpbin.org/patch',
b
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/patch')
done()
})
it('does basic http options request', async done => {
let res: httpm.HttpClientResponse = await _http.options(
'http://httpbin.org'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
done()
})
it('returns 404 for not found get request', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'http://httpbin.org/status/404'
)
expect(res.message.statusCode).toBe(404)
let body: string = await res.readBody()
done()
})
it('gets a json object', async () => {
let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
HttpBinData
>('https://httpbin.org/get')
expect(jsonObj.statusCode).toBe(200)
expect(jsonObj.result).toBeDefined()
expect(jsonObj.result.url).toBe('https://httpbin.org/get')
expect(jsonObj.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('getting a non existent json object returns null', async () => {
let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
HttpBinData
>('https://httpbin.org/status/404')
expect(jsonObj.statusCode).toBe(404)
expect(jsonObj.result).toBeNull()
})
it('posts a json object', async () => {
let res: any = {name: 'foo'}
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.postJson<
HttpBinData
>('https://httpbin.org/post', res)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result.url).toBe('https://httpbin.org/post')
expect(restRes.result.json.name).toBe('foo')
expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('puts a json object', async () => {
let res: any = {name: 'foo'}
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.putJson<
HttpBinData
>('https://httpbin.org/put', res)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result.url).toBe('https://httpbin.org/put')
expect(restRes.result.json.name).toBe('foo')
expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('patch a json object', async () => {
let res: any = {name: 'foo'}
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.patchJson<
HttpBinData
>('https://httpbin.org/patch', res)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result.url).toBe('https://httpbin.org/patch')
expect(restRes.result.json.name).toBe('foo')
expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
})

View File

@ -0,0 +1,115 @@
import * as httpm from '../_out'
import * as ifm from '../_out/interfaces'
describe('headers', () => {
let _http: httpm.HttpClient
beforeEach(() => {
_http = new httpm.HttpClient('http-client-tests')
})
it('preserves existing headers on getJson', async () => {
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj: ifm.ITypedResponse<any> = await _http.getJson<any>(
'https://httpbin.org/get',
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.getJson<any>('https://httpbin.org/get')
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on postJson', async () => {
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj: ifm.ITypedResponse<any> = await _http.postJson<any>(
'https://httpbin.org/post',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.postJson<any>(
'https://httpbin.org/post',
{}
)
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on putJson', async () => {
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj: ifm.ITypedResponse<any> = await _http.putJson<any>(
'https://httpbin.org/put',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.putJson<any>('https://httpbin.org/put', {})
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on patchJson', async () => {
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj: ifm.ITypedResponse<any> = await _http.patchJson<any>(
'https://httpbin.org/patch',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.patchJson<any>(
'https://httpbin.org/patch',
{}
)
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
})

View File

@ -0,0 +1,79 @@
import * as httpm from '../_out'
describe('basics', () => {
let _http: httpm.HttpClient
beforeEach(() => {
_http = new httpm.HttpClient('http-client-tests', [], {keepAlive: true})
})
afterEach(() => {
_http.dispose()
})
it('does basic http get request with keepAlive true', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
done()
})
it('does basic head request with keepAlive true', async done => {
let res: httpm.HttpClientResponse = await _http.head(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
done()
})
it('does basic http delete request with keepAlive true', async done => {
let res: httpm.HttpClientResponse = await _http.del(
'http://httpbin.org/delete'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
done()
})
it('does basic http post request with keepAlive true', async done => {
let b: string = 'Hello World!'
let res: httpm.HttpClientResponse = await _http.post(
'http://httpbin.org/post',
b
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/post')
done()
})
it('does basic http patch request with keepAlive true', async done => {
let b: string = 'Hello World!'
let res: httpm.HttpClientResponse = await _http.patch(
'http://httpbin.org/patch',
b
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/patch')
done()
})
it('does basic http options request with keepAlive true', async done => {
let res: httpm.HttpClientResponse = await _http.options(
'http://httpbin.org'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
done()
})
})

View File

@ -0,0 +1,228 @@
import * as http from 'http'
import * as httpm from '../_out'
import * as pm from '../_out/proxy'
import * as proxy from 'proxy'
import * as tunnelm from 'tunnel'
let _proxyConnects: string[]
let _proxyServer: http.Server
let _proxyUrl = 'http://127.0.0.1:8080'
describe('proxy', () => {
beforeAll(async () => {
// Start proxy server
_proxyServer = proxy()
await new Promise(resolve => {
const port = Number(_proxyUrl.split(':')[2])
_proxyServer.listen(port, () => resolve())
})
_proxyServer.on('connect', req => {
_proxyConnects.push(req.url)
})
})
beforeEach(() => {
_proxyConnects = []
_clearVars()
})
afterEach(() => {})
afterAll(async () => {
_clearVars()
// Stop proxy server
await new Promise(resolve => {
_proxyServer.once('close', () => resolve())
_proxyServer.close()
})
})
it('getProxyUrl does not return proxyUrl if variables not set', () => {
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined()
})
it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => {
process.env['https_proxy'] = 'https://myproxysvr'
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined()
})
it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => {
process.env['http_proxy'] = 'https://myproxysvr'
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined()
})
it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => {
process.env['http_proxy'] = 'http://myproxysvr'
let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined()
})
it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(new URL('https://myserver'))
expect(proxyUrl).toBeUndefined()
})
it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined()
})
it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(new URL('http://myserver'))
expect(proxyUrl).toBeUndefined()
})
it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined()
})
it('checkBypass returns true if host as no_proxy list', () => {
process.env['no_proxy'] = 'myserver'
let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host in no_proxy list with spaces', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host in no_proxy list with port', () => {
process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver'
let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host with port in no_proxy list without port', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver'
let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host in no_proxy list with default https port', () => {
process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver'
let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if host in no_proxy list with default http port', () => {
process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver'
let bypass = pm.checkBypass(new URL('http://myserver'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns false if host not in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy()
})
it('checkBypass returns false if empty no_proxy', () => {
process.env['no_proxy'] = ''
let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy()
})
it('HttpClient does basic http get request through proxy', async () => {
process.env['http_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient()
let res: httpm.HttpClientResponse = await httpClient.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(_proxyConnects).toEqual(['httpbin.org:80'])
})
it('HttoClient does basic http get request when bypass proxy', async () => {
process.env['http_proxy'] = _proxyUrl
process.env['no_proxy'] = 'httpbin.org'
const httpClient = new httpm.HttpClient()
let res: httpm.HttpClientResponse = await httpClient.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(_proxyConnects).toHaveLength(0)
})
it('HttpClient does basic https get request through proxy', async () => {
process.env['https_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient()
let res: httpm.HttpClientResponse = await httpClient.get(
'https://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
expect(_proxyConnects).toEqual(['httpbin.org:443'])
})
it('HttpClient does basic https get request when bypass proxy', async () => {
process.env['https_proxy'] = _proxyUrl
process.env['no_proxy'] = 'httpbin.org'
const httpClient = new httpm.HttpClient()
let res: httpm.HttpClientResponse = await httpClient.get(
'https://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
expect(_proxyConnects).toHaveLength(0)
})
it('proxyAuth not set in tunnel agent when authentication is not provided', async () => {
process.env['https_proxy'] = 'http://127.0.0.1:8080'
const httpClient = new httpm.HttpClient()
let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
console.log(agent)
expect(agent.proxyOptions.host).toBe('127.0.0.1')
expect(agent.proxyOptions.port).toBe('8080')
expect(agent.proxyOptions.proxyAuth).toBe(undefined)
})
it('proxyAuth is set in tunnel agent when authentication is provided', async () => {
process.env['https_proxy'] = 'http://user:password@127.0.0.1:8080'
const httpClient = new httpm.HttpClient()
let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
console.log(agent)
expect(agent.proxyOptions.host).toBe('127.0.0.1')
expect(agent.proxyOptions.port).toBe('8080')
expect(agent.proxyOptions.proxyAuth).toBe('user:password')
})
})
function _clearVars() {
delete process.env.http_proxy
delete process.env.HTTP_PROXY
delete process.env.https_proxy
delete process.env.HTTPS_PROXY
delete process.env.no_proxy
delete process.env.NO_PROXY
}

10494
packages/http-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "@actions/http-client",
"version": "1.0.11",
"description": "Actions Http Client",
"main": "index.js",
"scripts": {
"build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out",
"test": "jest",
"format": "prettier --write *.ts && prettier --write **/*.ts",
"format-check": "prettier --check *.ts && prettier --check **/*.ts",
"audit-check": "npm audit --audit-level=moderate"
},
"repository": {
"type": "git",
"url": "git+https://github.com/actions/http-client.git"
},
"keywords": [
"Actions",
"Http"
],
"author": "GitHub, Inc.",
"license": "MIT",
"bugs": {
"url": "https://github.com/actions/http-client/issues"
},
"homepage": "https://github.com/actions/http-client#readme",
"devDependencies": {
"@types/jest": "^25.1.4",
"@types/node": "^12.12.31",
"jest": "^25.1.0",
"prettier": "^2.0.4",
"proxy": "^1.0.1",
"ts-jest": "^25.2.1",
"typescript": "^3.8.3"
},
"dependencies": {
"tunnel": "0.0.6"
}
}

View File

@ -0,0 +1,86 @@
import ifm = require('./interfaces')
export class BasicCredentialHandler implements ifm.IRequestHandler {
username: string
password: string
constructor(username: string, password: string) {
this.username = username
this.password = password
}
prepareRequest(options: any): void {
options.headers['Authorization'] =
'Basic ' +
Buffer.from(this.username + ':' + this.password).toString('base64')
}
// This handler cannot handle 401
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false
}
handleAuthentication(
httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
}
}
export class BearerCredentialHandler implements ifm.IRequestHandler {
token: string
constructor(token: string) {
this.token = token
}
// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options: any): void {
options.headers['Authorization'] = 'Bearer ' + this.token
}
// This handler cannot handle 401
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false
}
handleAuthentication(
httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
}
}
export class PersonalAccessTokenCredentialHandler
implements ifm.IRequestHandler {
token: string
constructor(token: string) {
this.token = token
}
// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options: any): void {
options.headers['Authorization'] =
'Basic ' + Buffer.from('PAT:' + this.token).toString('base64')
}
// This handler cannot handle 401
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false
}
handleAuthentication(
httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
}
}

View File

@ -0,0 +1,768 @@
import http = require('http')
import https = require('https')
import ifm = require('./interfaces')
import pm = require('./proxy')
let tunnel: any
export enum HttpCodes {
OK = 200,
MultipleChoices = 300,
MovedPermanently = 301,
ResourceMoved = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
SwitchProxy = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
TooManyRequests = 429,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504
}
export enum Headers {
Accept = 'accept',
ContentType = 'content-type'
}
export enum MediaTypes {
ApplicationJson = 'application/json'
}
/**
* Returns the proxy URL, depending upon the supplied url and proxy environment variables.
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/
export function getProxyUrl(serverUrl: string): string {
let proxyUrl = pm.getProxyUrl(new URL(serverUrl))
return proxyUrl ? proxyUrl.href : ''
}
const HttpRedirectCodes: number[] = [
HttpCodes.MovedPermanently,
HttpCodes.ResourceMoved,
HttpCodes.SeeOther,
HttpCodes.TemporaryRedirect,
HttpCodes.PermanentRedirect
]
const HttpResponseRetryCodes: number[] = [
HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout
]
const RetryableHttpVerbs: string[] = ['OPTIONS', 'GET', 'DELETE', 'HEAD']
const ExponentialBackoffCeiling = 10
const ExponentialBackoffTimeSlice = 5
export class HttpClientError extends Error {
constructor(message: string, statusCode: number) {
super(message)
this.name = 'HttpClientError'
this.statusCode = statusCode
Object.setPrototypeOf(this, HttpClientError.prototype)
}
public statusCode: number
public result?: any
}
export class HttpClientResponse implements ifm.IHttpClientResponse {
constructor(message: http.IncomingMessage) {
this.message = message
}
public message: http.IncomingMessage
readBody(): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
let output = Buffer.alloc(0)
this.message.on('data', (chunk: Buffer) => {
output = Buffer.concat([output, chunk])
})
this.message.on('end', () => {
resolve(output.toString())
})
})
}
}
export function isHttps(requestUrl: string) {
let parsedUrl: URL = new URL(requestUrl)
return parsedUrl.protocol === 'https:'
}
export class HttpClient {
userAgent: string | undefined
handlers: ifm.IRequestHandler[]
requestOptions: ifm.IRequestOptions
private _ignoreSslError: boolean = false
private _socketTimeout: number
private _allowRedirects: boolean = true
private _allowRedirectDowngrade: boolean = false
private _maxRedirects: number = 50
private _allowRetries: boolean = false
private _maxRetries: number = 1
private _agent
private _proxyAgent
private _keepAlive: boolean = false
private _disposed: boolean = false
constructor(
userAgent?: string,
handlers?: ifm.IRequestHandler[],
requestOptions?: ifm.IRequestOptions
) {
this.userAgent = userAgent
this.handlers = handlers || []
this.requestOptions = requestOptions
if (requestOptions) {
if (requestOptions.ignoreSslError != null) {
this._ignoreSslError = requestOptions.ignoreSslError
}
this._socketTimeout = requestOptions.socketTimeout
if (requestOptions.allowRedirects != null) {
this._allowRedirects = requestOptions.allowRedirects
}
if (requestOptions.allowRedirectDowngrade != null) {
this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade
}
if (requestOptions.maxRedirects != null) {
this._maxRedirects = Math.max(requestOptions.maxRedirects, 0)
}
if (requestOptions.keepAlive != null) {
this._keepAlive = requestOptions.keepAlive
}
if (requestOptions.allowRetries != null) {
this._allowRetries = requestOptions.allowRetries
}
if (requestOptions.maxRetries != null) {
this._maxRetries = requestOptions.maxRetries
}
}
}
public options(
requestUrl: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('OPTIONS', requestUrl, null, additionalHeaders || {})
}
public get(
requestUrl: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('GET', requestUrl, null, additionalHeaders || {})
}
public del(
requestUrl: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('DELETE', requestUrl, null, additionalHeaders || {})
}
public post(
requestUrl: string,
data: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('POST', requestUrl, data, additionalHeaders || {})
}
public patch(
requestUrl: string,
data: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('PATCH', requestUrl, data, additionalHeaders || {})
}
public put(
requestUrl: string,
data: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('PUT', requestUrl, data, additionalHeaders || {})
}
public head(
requestUrl: string,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request('HEAD', requestUrl, null, additionalHeaders || {})
}
public sendStream(
verb: string,
requestUrl: string,
stream: NodeJS.ReadableStream,
additionalHeaders?: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
return this.request(verb, requestUrl, stream, additionalHeaders)
}
/**
* Gets a typed object from an endpoint
* Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise
*/
public async getJson<T>(
requestUrl: string,
additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.ITypedResponse<T>> {
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.Accept,
MediaTypes.ApplicationJson
)
let res: ifm.IHttpClientResponse = await this.get(
requestUrl,
additionalHeaders
)
return this._processResponse<T>(res, this.requestOptions)
}
public async postJson<T>(
requestUrl: string,
obj: any,
additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.ITypedResponse<T>> {
let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.Accept,
MediaTypes.ApplicationJson
)
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.ContentType,
MediaTypes.ApplicationJson
)
let res: ifm.IHttpClientResponse = await this.post(
requestUrl,
data,
additionalHeaders
)
return this._processResponse<T>(res, this.requestOptions)
}
public async putJson<T>(
requestUrl: string,
obj: any,
additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.ITypedResponse<T>> {
let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.Accept,
MediaTypes.ApplicationJson
)
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.ContentType,
MediaTypes.ApplicationJson
)
let res: ifm.IHttpClientResponse = await this.put(
requestUrl,
data,
additionalHeaders
)
return this._processResponse<T>(res, this.requestOptions)
}
public async patchJson<T>(
requestUrl: string,
obj: any,
additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.ITypedResponse<T>> {
let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.Accept,
MediaTypes.ApplicationJson
)
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
additionalHeaders,
Headers.ContentType,
MediaTypes.ApplicationJson
)
let res: ifm.IHttpClientResponse = await this.patch(
requestUrl,
data,
additionalHeaders
)
return this._processResponse<T>(res, this.requestOptions)
}
/**
* Makes a raw http request.
* All other methods such as get, post, patch, and request ultimately call this.
* Prefer get, del, post and patch
*/
public async request(
verb: string,
requestUrl: string,
data: string | NodeJS.ReadableStream,
headers: ifm.IHeaders
): Promise<ifm.IHttpClientResponse> {
if (this._disposed) {
throw new Error('Client has already been disposed.')
}
let parsedUrl = new URL(requestUrl)
let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers)
// Only perform retries on reads since writes may not be idempotent.
let maxTries: number =
this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
? this._maxRetries + 1
: 1
let numTries: number = 0
let response: HttpClientResponse
while (numTries < maxTries) {
response = await this.requestRaw(info, data)
// Check if it's an authentication challenge
if (
response &&
response.message &&
response.message.statusCode === HttpCodes.Unauthorized
) {
let authenticationHandler: ifm.IRequestHandler
for (let i = 0; i < this.handlers.length; i++) {
if (this.handlers[i].canHandleAuthentication(response)) {
authenticationHandler = this.handlers[i]
break
}
}
if (authenticationHandler) {
return authenticationHandler.handleAuthentication(this, info, data)
} else {
// We have received an unauthorized response but have no handlers to handle it.
// Let the response return to the caller.
return response
}
}
let redirectsRemaining: number = this._maxRedirects
while (
HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
this._allowRedirects &&
redirectsRemaining > 0
) {
const redirectUrl: string | null = response.message.headers['location']
if (!redirectUrl) {
// if there's no location to redirect to, we won't
break
}
let parsedRedirectUrl = new URL(redirectUrl)
if (
parsedUrl.protocol == 'https:' &&
parsedUrl.protocol != parsedRedirectUrl.protocol &&
!this._allowRedirectDowngrade
) {
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.'
)
}
// we need to finish reading the response before reassigning response
// which will leak the open socket.
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
info = this._prepareRequest(verb, parsedRedirectUrl, headers)
response = await this.requestRaw(info, data)
redirectsRemaining--
}
if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) {
// If not a retry code, return immediately instead of retrying
return response
}
numTries += 1
if (numTries < maxTries) {
await response.readBody()
await this._performExponentialBackoff(numTries)
}
}
return response
}
/**
* Needs to be called if keepAlive is set to true in request options.
*/
public dispose() {
if (this._agent) {
this._agent.destroy()
}
this._disposed = true
}
/**
* Raw request.
* @param info
* @param data
*/
public requestRaw(
info: ifm.IRequestInfo,
data: string | NodeJS.ReadableStream
): Promise<ifm.IHttpClientResponse> {
return new Promise<ifm.IHttpClientResponse>((resolve, reject) => {
let callbackForResult = function (
err: any,
res: ifm.IHttpClientResponse
) {
if (err) {
reject(err)
}
resolve(res)
}
this.requestRawWithCallback(info, data, callbackForResult)
})
}
/**
* Raw request with callback.
* @param info
* @param data
* @param onResult
*/
public requestRawWithCallback(
info: ifm.IRequestInfo,
data: string | NodeJS.ReadableStream,
onResult: (err: any, res: ifm.IHttpClientResponse) => void
): void {
let socket
if (typeof data === 'string') {
info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
}
let callbackCalled: boolean = false
let handleResult = (err: any, res: HttpClientResponse) => {
if (!callbackCalled) {
callbackCalled = true
onResult(err, res)
}
}
let req: http.ClientRequest = info.httpModule.request(
info.options,
(msg: http.IncomingMessage) => {
let res: HttpClientResponse = new HttpClientResponse(msg)
handleResult(null, res)
}
)
req.on('socket', sock => {
socket = sock
})
// If we ever get disconnected, we want the socket to timeout eventually
req.setTimeout(this._socketTimeout || 3 * 60000, () => {
if (socket) {
socket.end()
}
handleResult(new Error('Request timeout: ' + info.options.path), null)
})
req.on('error', function (err) {
// err has statusCode property
// res should have headers
handleResult(err, null)
})
if (data && typeof data === 'string') {
req.write(data, 'utf8')
}
if (data && typeof data !== 'string') {
data.on('close', function () {
req.end()
})
data.pipe(req)
} else {
req.end()
}
}
/**
* Gets an http agent. This function is useful when you need an http agent that handles
* routing through a proxy server - depending upon the url and proxy environment variables.
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/
public getAgent(serverUrl: string): http.Agent {
let parsedUrl = new URL(serverUrl)
return this._getAgent(parsedUrl)
}
private _prepareRequest(
method: string,
requestUrl: URL,
headers: ifm.IHeaders
): ifm.IRequestInfo {
const info: ifm.IRequestInfo = <ifm.IRequestInfo>{}
info.parsedUrl = requestUrl
const usingSsl: boolean = info.parsedUrl.protocol === 'https:'
info.httpModule = usingSsl ? https : http
const defaultPort: number = usingSsl ? 443 : 80
info.options = <http.RequestOptions>{}
info.options.host = info.parsedUrl.hostname
info.options.port = info.parsedUrl.port
? parseInt(info.parsedUrl.port)
: defaultPort
info.options.path =
(info.parsedUrl.pathname || '') + (info.parsedUrl.search || '')
info.options.method = method
info.options.headers = this._mergeHeaders(headers)
if (this.userAgent != null) {
info.options.headers['user-agent'] = this.userAgent
}
info.options.agent = this._getAgent(info.parsedUrl)
// gives handlers an opportunity to participate
if (this.handlers) {
this.handlers.forEach(handler => {
handler.prepareRequest(info.options)
})
}
return info
}
private _mergeHeaders(headers: ifm.IHeaders): ifm.IHeaders {
const lowercaseKeys = obj =>
Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
if (this.requestOptions && this.requestOptions.headers) {
return Object.assign(
{},
lowercaseKeys(this.requestOptions.headers),
lowercaseKeys(headers)
)
}
return lowercaseKeys(headers || {})
}
private _getExistingOrDefaultHeader(
additionalHeaders: ifm.IHeaders,
header: string,
_default: string
) {
const lowercaseKeys = obj =>
Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
let clientHeader: string
if (this.requestOptions && this.requestOptions.headers) {
clientHeader = lowercaseKeys(this.requestOptions.headers)[header]
}
return additionalHeaders[header] || clientHeader || _default
}
private _getAgent(parsedUrl: URL): http.Agent {
let agent
let proxyUrl: URL = pm.getProxyUrl(parsedUrl)
let useProxy = proxyUrl && proxyUrl.hostname
if (this._keepAlive && useProxy) {
agent = this._proxyAgent
}
if (this._keepAlive && !useProxy) {
agent = this._agent
}
// if agent is already assigned use that agent.
if (!!agent) {
return agent
}
const usingSsl = parsedUrl.protocol === 'https:'
let maxSockets = 100
if (!!this.requestOptions) {
maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets
}
if (useProxy) {
// If using proxy, need tunnel
if (!tunnel) {
tunnel = require('tunnel')
}
const agentOptions = {
maxSockets: maxSockets,
keepAlive: this._keepAlive,
proxy: {
...((proxyUrl.username || proxyUrl.password) && {
proxyAuth: `${proxyUrl.username}:${proxyUrl.password}`
}),
host: proxyUrl.hostname,
port: proxyUrl.port
}
}
let tunnelAgent: Function
const overHttps = proxyUrl.protocol === 'https:'
if (usingSsl) {
tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp
} else {
tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp
}
agent = tunnelAgent(agentOptions)
this._proxyAgent = agent
}
// if reusing agent across request and tunneling agent isn't assigned create a new agent
if (this._keepAlive && !agent) {
const options = {keepAlive: this._keepAlive, maxSockets: maxSockets}
agent = usingSsl ? new https.Agent(options) : new http.Agent(options)
this._agent = agent
}
// if not using private agent and tunnel agent isn't setup then use global agent
if (!agent) {
agent = usingSsl ? https.globalAgent : http.globalAgent
}
if (usingSsl && this._ignoreSslError) {
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
// we have to cast it to any and change it directly
agent.options = Object.assign(agent.options || {}, {
rejectUnauthorized: false
})
}
return agent
}
private _performExponentialBackoff(retryNumber: number): Promise<void> {
retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber)
const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber)
return new Promise(resolve => setTimeout(() => resolve(), ms))
}
private static dateTimeDeserializer(key: any, value: any): any {
if (typeof value === 'string') {
let a = new Date(value)
if (!isNaN(a.valueOf())) {
return a
}
}
return value
}
private async _processResponse<T>(
res: ifm.IHttpClientResponse,
options: ifm.IRequestOptions
): Promise<ifm.ITypedResponse<T>> {
return new Promise<ifm.ITypedResponse<T>>(async (resolve, reject) => {
const statusCode: number = res.message.statusCode
const response: ifm.ITypedResponse<T> = {
statusCode: statusCode,
result: null,
headers: {}
}
// not found leads to null obj returned
if (statusCode == HttpCodes.NotFound) {
resolve(response)
}
let obj: any
let contents: string
// get the result from the body
try {
contents = await res.readBody()
if (contents && contents.length > 0) {
if (options && options.deserializeDates) {
obj = JSON.parse(contents, HttpClient.dateTimeDeserializer)
} else {
obj = JSON.parse(contents)
}
response.result = obj
}
response.headers = res.message.headers
} catch (err) {
// Invalid resource (contents not json); leaving result obj null
}
// note that 3xx redirects are handled by the http layer.
if (statusCode > 299) {
let msg: string
// if exception/error in body, attempt to get better error
if (obj && obj.message) {
msg = obj.message
} else if (contents && contents.length > 0) {
// it may be the case that the exception is in the body message as string
msg = contents
} else {
msg = 'Failed request: (' + statusCode + ')'
}
let err = new HttpClientError(msg, statusCode)
err.result = response.result
reject(err)
} else {
resolve(response)
}
})
}
}

View File

@ -0,0 +1,98 @@
import http = require('http')
export interface IHeaders {
[key: string]: any
}
export interface IHttpClient {
options(
requestUrl: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
get(
requestUrl: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
del(
requestUrl: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
post(
requestUrl: string,
data: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
patch(
requestUrl: string,
data: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
put(
requestUrl: string,
data: string,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
sendStream(
verb: string,
requestUrl: string,
stream: NodeJS.ReadableStream,
additionalHeaders?: IHeaders
): Promise<IHttpClientResponse>
request(
verb: string,
requestUrl: string,
data: string | NodeJS.ReadableStream,
headers: IHeaders
): Promise<IHttpClientResponse>
requestRaw(
info: IRequestInfo,
data: string | NodeJS.ReadableStream
): Promise<IHttpClientResponse>
requestRawWithCallback(
info: IRequestInfo,
data: string | NodeJS.ReadableStream,
onResult: (err: any, res: IHttpClientResponse) => void
): void
}
export interface IRequestHandler {
prepareRequest(options: http.RequestOptions): void
canHandleAuthentication(response: IHttpClientResponse): boolean
handleAuthentication(
httpClient: IHttpClient,
requestInfo: IRequestInfo,
objs
): Promise<IHttpClientResponse>
}
export interface IHttpClientResponse {
message: http.IncomingMessage
readBody(): Promise<string>
}
export interface IRequestInfo {
options: http.RequestOptions
parsedUrl: URL
httpModule: any
}
export interface IRequestOptions {
headers?: IHeaders
socketTimeout?: number
ignoreSslError?: boolean
allowRedirects?: boolean
allowRedirectDowngrade?: boolean
maxRedirects?: number
maxSockets?: number
keepAlive?: boolean
deserializeDates?: boolean
// Allows retries only on Read operations (since writes may not be idempotent)
allowRetries?: boolean
maxRetries?: number
}
export interface ITypedResponse<T> {
statusCode: number
result: T | null
headers: Object
}

View File

@ -0,0 +1,60 @@
export function getProxyUrl(reqUrl: URL): URL | undefined {
let usingSsl = reqUrl.protocol === 'https:'
let proxyUrl: URL
if (checkBypass(reqUrl)) {
return proxyUrl
}
let proxyVar: string
if (usingSsl) {
proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY']
} else {
proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY']
}
if (proxyVar) {
proxyUrl = new URL(proxyVar)
}
return proxyUrl
}
export function checkBypass(reqUrl: URL): boolean {
if (!reqUrl.hostname) {
return false
}
let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''
if (!noProxy) {
return false
}
// Determine the request port
let reqPort: number
if (reqUrl.port) {
reqPort = Number(reqUrl.port)
} else if (reqUrl.protocol === 'http:') {
reqPort = 80
} else if (reqUrl.protocol === 'https:') {
reqPort = 443
}
// Format the request hostname and hostname with port
let upperReqHosts = [reqUrl.hostname.toUpperCase()]
if (typeof reqPort === 'number') {
upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`)
}
// Compare request host against noproxy
for (let upperNoProxyItem of noProxy
.split(',')
.map(x => x.trim().toUpperCase())
.filter(x => x)) {
if (upperReqHosts.some(x => x === upperNoProxyItem)) {
return true
}
}
return false
}