mirror of
https://github.com/actions/http-client.git
synced 2025-04-21 09:42:29 +00:00
initial commit
This commit is contained in:
commit
0fc3b75a6d
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
_out
|
||||
node_modules
|
||||
.DS_Store
|
||||
testoutput.txt
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
Typed Rest 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.
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
# Typed HTTP Client with TypeScript Typings
|
||||
|
||||
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)
|
||||
- Basic, Bearer and PAT Support out of the box. Extensible handlers for others.
|
||||
- Proxy support, just works with actions and the runner
|
||||
- Redirects supported
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
set NODE_DEBUG=http
|
||||
```
|
||||
|
||||
## Node support
|
||||
|
||||
The http-client is built using the latest LTS version of Node 12. We also support the latest LTS for Node 6, 8 and Node 10.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome PRs. Please create an issue and if applicable, a design before proceeding with code.
|
||||
|
||||
To build:
|
||||
|
||||
```bash
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
```bash
|
||||
$ npm test
|
||||
```
|
57
__tests__/auth.test.ts
Normal file
57
__tests__/auth.test.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as httpm from '../_out';
|
||||
import * as path from 'path';
|
||||
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("https://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("https://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("https://httpbin.org/get");
|
||||
});
|
||||
})
|
182
__tests__/basics.test.ts
Normal file
182
__tests__/basics.test.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import * as httpm from '../_out';
|
||||
import * as path from 'path';
|
||||
import * as am from '../_out/auth';
|
||||
import * as fs from 'fs';
|
||||
|
||||
let sampleFilePath: string = path.join(__dirname, 'testoutput.txt');
|
||||
|
||||
describe('basics', () => {
|
||||
let _http: httpm.HttpClient;
|
||||
let _httpbin: httpm.HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
_http = new httpm.HttpClient('http-client-tests');
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
})
|
||||
|
||||
it('constructs', () => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('typed-test-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() => {
|
||||
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("https://httpbin.org/get");
|
||||
expect(obj.headers["User-Agent"]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does basic http get request with no user agent', async() => {
|
||||
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("https://httpbin.org/get");
|
||||
expect(obj.headers["User-Agent"]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does basic https get request', async() => {
|
||||
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");
|
||||
});
|
||||
|
||||
it('does basic http get request with default headers', async() => {
|
||||
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("https://httpbin.org/get");
|
||||
});
|
||||
|
||||
it('does basic http get request with merged headers', async() => {
|
||||
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("https://httpbin.org/get");
|
||||
});
|
||||
|
||||
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() => {
|
||||
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");
|
||||
});
|
||||
|
||||
it('does basic get request with redirects (303)', async() => {
|
||||
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");
|
||||
});
|
||||
|
||||
it('returns 404 for not found get request on redirect', async() => {
|
||||
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();
|
||||
});
|
||||
|
||||
it('does not follow redirects if disabled', async() => {
|
||||
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();
|
||||
});
|
||||
|
||||
it('does basic head request', async() => {
|
||||
let res: httpm.HttpClientResponse = await _http.head('http://httpbin.org/get');
|
||||
expect(res.message.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('does basic http delete request', async() => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('does basic http post request', async() => {
|
||||
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("https://httpbin.org/post");
|
||||
});
|
||||
|
||||
it('does basic http patch request', async() => {
|
||||
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("https://httpbin.org/patch");
|
||||
});
|
||||
|
||||
it('does basic http options request', async() => {
|
||||
let res: httpm.HttpClientResponse = await _http.options('http://httpbin.org');
|
||||
expect(res.message.statusCode).toBe(200);
|
||||
let body: string = await res.readBody();
|
||||
});
|
||||
|
||||
it('returns 404 for not found get request', async() => {
|
||||
let res: httpm.HttpClientResponse = await _http.get('http://httpbin.org/status/404');
|
||||
expect(res.message.statusCode).toBe(404);
|
||||
let body: string = await res.readBody();
|
||||
});
|
||||
})
|
65
__tests__/keepalive.test.ts
Normal file
65
__tests__/keepalive.test.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import * as httpm from '../_out';
|
||||
import * as path from 'path';
|
||||
import * as am from '../_out/auth';
|
||||
import * as fs from 'fs';
|
||||
|
||||
let sampleFilePath: string = path.join(__dirname, 'testoutput.txt');
|
||||
|
||||
describe('basics', () => {
|
||||
let _http: httpm.HttpClient;
|
||||
let _httpbin: httpm.HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
_http = new httpm.HttpClient('typed-test-client-tests', [], { keepAlive: true });
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
})
|
||||
|
||||
it('does basic http get request with keepAlive true', async() => {
|
||||
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("https://httpbin.org/get");
|
||||
});
|
||||
|
||||
it('does basic head request with keepAlive true', async() => {
|
||||
let res: httpm.HttpClientResponse = await _http.head('http://httpbin.org/get');
|
||||
expect(res.message.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('does basic http delete request with keepAlive true', async() => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('does basic http post request with keepAlive true', async() => {
|
||||
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("https://httpbin.org/post");
|
||||
});
|
||||
|
||||
it('does basic http patch request with keepAlive true', async() => {
|
||||
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("https://httpbin.org/patch");
|
||||
});
|
||||
|
||||
it('does basic http options request with keepAlive true', async() => {
|
||||
let res: httpm.HttpClientResponse = await _http.options('http://httpbin.org');
|
||||
expect(res.message.statusCode).toBe(200);
|
||||
let body: string = await res.readBody();
|
||||
});
|
||||
});
|
96
__tests__/proxy.test.ts
Normal file
96
__tests__/proxy.test.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import * as pm from '../_out/proxy';
|
||||
import * as url from 'url';
|
||||
|
||||
describe('proxy', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if variables not set', () => {
|
||||
_clearVars();
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('returns proxyUrl if https_proxy set for https url', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com'));
|
||||
expect(proxyUrl).toBeDefined();
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if http_proxy set for https url', () => {
|
||||
_clearVars();
|
||||
process.env["http_proxy"] = "https://myproxysvr";
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('returns proxyUrl if http_proxy set for http url', () => {
|
||||
_clearVars();
|
||||
process.env["http_proxy"] = "http://myproxysvr";
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('http://github.com'));
|
||||
expect(proxyUrl).toBeDefined();
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if only host as no_proxy list', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = "myserver"
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://myserver'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if host in no_proxy list', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = "otherserver,myserver,anotherserver:8080"
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://myserver'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if host in no_proxy list with spaces', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = "otherserver, myserver ,anotherserver:8080"
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://myserver'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('does not return proxyUrl if host in no_proxy list with ports', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = "otherserver, myserver:8080 ,anotherserver"
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://myserver:8080'));
|
||||
expect(proxyUrl).toBeUndefined();
|
||||
})
|
||||
|
||||
it('returns proxyUrl if https_proxy set and not in no_proxy list', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = "otherserver, myserver ,anotherserver:8080"
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com'));
|
||||
expect(proxyUrl).toBeDefined();
|
||||
})
|
||||
|
||||
it('returns proxyUrl if https_proxy set empty no_proxy set', () => {
|
||||
_clearVars();
|
||||
process.env["https_proxy"] = "https://myproxysvr";
|
||||
process.env["no_proxy"] = ""
|
||||
let proxyUrl = pm.getProxyUrl(url.parse('https://github.com'));
|
||||
expect(proxyUrl).toBeDefined();
|
||||
})
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
71
auth.ts
Normal file
71
auth.ts
Normal file
@ -0,0 +1,71 @@
|
||||
|
||||
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 ' + new Buffer(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 ' + new Buffer('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;
|
||||
}
|
||||
}
|
456
index.ts
Normal file
456
index.ts
Normal file
@ -0,0 +1,456 @@
|
||||
import url = require("url");
|
||||
import http = require("http");
|
||||
import https = require("https");
|
||||
import ifm = require('./interfaces');
|
||||
import pm = require('./proxy');
|
||||
|
||||
let fs: any;
|
||||
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,
|
||||
InternalServerError = 500,
|
||||
NotImplemented = 501,
|
||||
BadGateway = 502,
|
||||
ServiceUnavailable = 503,
|
||||
GatewayTimeout = 504,
|
||||
}
|
||||
|
||||
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 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.Url = url.parse(requestUrl);
|
||||
return parsedUrl.protocol === 'https:';
|
||||
}
|
||||
|
||||
export class HttpClient {
|
||||
userAgent: string | null | 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = url.parse(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 = url.parse(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();
|
||||
|
||||
// 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;
|
||||
|
||||
let isDataString = typeof (data) === 'string';
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private _prepareRequest(method: string, requestUrl: url.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 _getAgent(parsedUrl: url.Url) {
|
||||
let agent;
|
||||
let proxyUrl: url.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: {
|
||||
proxyAuth: proxyUrl.auth,
|
||||
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));
|
||||
}
|
||||
}
|
48
interfaces.ts
Normal file
48
interfaces.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import http = require("http");
|
||||
import url = require("url");
|
||||
|
||||
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.Url;
|
||||
httpModule: any;
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
headers?: IHeaders;
|
||||
socketTimeout?: number;
|
||||
ignoreSslError?: boolean;
|
||||
allowRedirects?: boolean;
|
||||
allowRedirectDowngrade?: boolean;
|
||||
maxRedirects?: number;
|
||||
maxSockets?: number;
|
||||
keepAlive?: boolean;
|
||||
// Allows retries only on Read operations (since writes may not be idempotent)
|
||||
allowRetries?: boolean;
|
||||
maxRetries?: number;
|
||||
}
|
10
jest.config.js
Normal file
10
jest.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
4995
package-lock.json
generated
Normal file
4995
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "http-client",
|
||||
"version": "0.5.0",
|
||||
"description": "Actions Http Client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out",
|
||||
"test": "jest"
|
||||
},
|
||||
"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": "^24.0.25",
|
||||
"@types/node": "^13.1.5",
|
||||
"@types/shelljs": "^0.8.6",
|
||||
"jest": "^24.9.0",
|
||||
"nock": "^11.7.2",
|
||||
"shelljs": "^0.8.3",
|
||||
"ts-jest": "^24.3.0",
|
||||
"typescript": "^3.7.4"
|
||||
}
|
||||
}
|
43
proxy.ts
Normal file
43
proxy.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as url from 'url';
|
||||
|
||||
export function getProxyUrl(reqUrl: url.Url): url.Url {
|
||||
let usingSsl = reqUrl.protocol === 'https:';
|
||||
|
||||
let noProxy: string = process.env["no_proxy"] ||
|
||||
process.env["NO_PROXY"];
|
||||
|
||||
let bypass: boolean;
|
||||
if (noProxy && typeof noProxy === 'string') {
|
||||
let bypassList = noProxy.split(',');
|
||||
for (let i=0; i < bypassList.length; i++) {
|
||||
let item = bypassList[i];
|
||||
if (item &&
|
||||
typeof item === "string" &&
|
||||
reqUrl.host.toLocaleLowerCase() == item.trim().toLocaleLowerCase()) {
|
||||
bypass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let proxyUrl: url.Url;
|
||||
if (bypass) {
|
||||
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 = url.parse(proxyVar);
|
||||
}
|
||||
|
||||
return proxyUrl;
|
||||
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": [ "node_modules/@types" ],
|
||||
"declaration": true,
|
||||
"outDir": "_out",
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"auth.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user