diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e716834..26d62f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,15 @@ name: http-tests -on: +on: push: branches: - master paths-ignore: - - '**.md' + - "**.md" pull_request: paths-ignore: - - '**.md' + - "**.md" jobs: - build: name: Build @@ -23,22 +22,25 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} - - name: Setup node - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} + - name: npm install + run: npm install - - name: npm install - run: npm install + - name: Compile + run: npm run build - - name: Compile - run: npm run build + - name: Format + run: npm run format-check - - name: npm test - run: npm test + - name: npm test + run: npm test - - name: audit security - run: npm audit + - name: audit security + run: npm audit diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0921256 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +_out/* diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 5c0e607..e1bc267 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -1,56 +1,73 @@ -import * as httpm from '../_out'; -import * as am from '../_out/auth'; +import * as httpm from "../_out"; +import * as am from "../_out/auth"; -describe('auth', () => { - beforeEach(() => { +describe("auth", () => { + beforeEach(() => {}); - }) - - afterEach(() => { + 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 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); + 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; + let creds: string = Buffer.from( + auth.substring("Basic ".length), + "base64" + ).toString(); + expect(creds).toBe("PAT:" + token); + expect(obj.url).toBe("http://httpbin.org/get"); + }); - 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"); - }); -}) + 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"); + }); +}); diff --git a/__tests__/basics.test.ts b/__tests__/basics.test.ts index eb1c3e9..8b1a6a5 100644 --- a/__tests__/basics.test.ts +++ b/__tests__/basics.test.ts @@ -1,256 +1,333 @@ -import * as httpm from '../_out'; -import * as ifm from '../_out/interfaces' -import * as path from 'path'; -import * as fs from 'fs'; +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'); +let sampleFilePath: string = path.join(__dirname, "testoutput.txt"); interface HttpBinData { - url: string; - data: any; - json: any; - headers: any; - args?: any + url: string; + data: any; + json: any; + headers: any; + args?: any; } -describe('basics', () => { - let _http: httpm.HttpClient; +describe("basics", () => { + let _http: httpm.HttpClient; - beforeEach(() => { - _http = new httpm.HttpClient('http-client-tests'); - }) - - afterEach(() => { + beforeEach(() => { + _http = new httpm.HttpClient("http-client-tests"); + }); - }) - - it('constructs', () => { - let http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests'); - expect(http).toBeDefined(); - }); + afterEach(() => {}); - // 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("constructs", () => { + let http: httpm.HttpClient = new httpm.HttpClient("thttp-client-tests"); + expect(http).toBeDefined(); + }); - 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(); - }); + // 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 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", 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 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 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 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("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('pipes a get request', () => { - return new Promise(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 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", + }, }); - - 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(); + 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('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("pipes a get request", () => { + return new Promise(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('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 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 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 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('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("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 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 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 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 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 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 = await _http.getJson('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 = await _http.getJson('https://httpbin.org/status/404'); - expect(jsonObj.statusCode).toBe(404); - expect(jsonObj.result).toBeNull(); - }); + 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('posts a json object', async() => { - let res: any = { name: 'foo' }; - let restRes: ifm.ITypedResponse = await _http.postJson('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("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('puts a json object', async() => { - let res: any = { name: 'foo' }; - let restRes: ifm.ITypedResponse = await _http.putJson('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'); + 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(); + }); - 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 = await _http.patchJson('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); - }); + 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 = 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 = 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 = 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 = 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 = 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 + ); + }); }); diff --git a/__tests__/headers.test.ts b/__tests__/headers.test.ts index ca36191..cdbe47e 100644 --- a/__tests__/headers.test.ts +++ b/__tests__/headers.test.ts @@ -1,79 +1,115 @@ -import * as httpm from '../_out'; -import * as ifm from '../_out/interfaces' +import * as httpm from "../_out"; +import * as ifm from "../_out/interfaces"; -describe('headers', () => { - let _http: httpm.HttpClient; +describe("headers", () => { + let _http: httpm.HttpClient; - beforeEach(() => { - _http = new httpm.HttpClient('http-client-tests'); - }); + 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 = await _http.getJson('https://httpbin.org/get', additionalHeaders); - expect(jsonObj.result.headers["Accept"]).toBe("foo"); - expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(httpm.MediaTypes.ApplicationJson); + it("preserves existing headers on getJson", async () => { + let additionalHeaders = { [httpm.Headers.Accept]: "foo" }; + let jsonObj: ifm.ITypedResponse = await _http.getJson( + "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('https://httpbin.org/get'); - expect(jsonObj.result.headers["Accept"]).toBe("baz"); - 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("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 = await _http.postJson('https://httpbin.org/post', {}, additionalHeaders); - expect(jsonObj.result.headers["Accept"]).toBe("foo"); - 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 = await _http.postJson( + "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('https://httpbin.org/post', {}); - expect(jsonObj.result.headers["Accept"]).toBe("baz"); - 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( + "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 = await _http.putJson('https://httpbin.org/put', {}, additionalHeaders); - expect(jsonObj.result.headers["Accept"]).toBe("foo"); - 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 = await _http.putJson( + "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('https://httpbin.org/put', {}); - expect(jsonObj.result.headers["Accept"]).toBe("baz"); - 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("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 = await _http.patchJson('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('https://httpbin.org/patch', {}); - 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 = await _http.patchJson( + "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( + "https://httpbin.org/patch", + {} + ); + expect(jsonObj.result.headers["Accept"]).toBe("baz"); + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ); + }); }); diff --git a/__tests__/keepalive.test.ts b/__tests__/keepalive.test.ts index b532190..5fe1a7d 100644 --- a/__tests__/keepalive.test.ts +++ b/__tests__/keepalive.test.ts @@ -1,65 +1,79 @@ -import * as httpm from '../_out'; +import * as httpm from "../_out"; -describe('basics', () => { - let _http: httpm.HttpClient; +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(); - }); + beforeEach(() => { + _http = new httpm.HttpClient("http-client-tests", [], { keepAlive: true }); + }); - 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(); - }); + afterEach(() => { + _http.dispose(); + }); - 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(); - }); + 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(); + }); }); diff --git a/__tests__/proxy.test.ts b/__tests__/proxy.test.ts index c085a53..d4acfe6 100644 --- a/__tests__/proxy.test.ts +++ b/__tests__/proxy.test.ts @@ -1,201 +1,208 @@ -import * as http from 'http' -import * as httpm from '../_out'; -import * as pm from '../_out/proxy'; -import * as proxy from 'proxy' -import * as url from 'url'; - -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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('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(url.parse('https://github.com')); - expect(bypass).toBeFalsy(); - }) - - it('checkBypass returns false if empty no_proxy', () => { - process.env["no_proxy"] = "" - let bypass = pm.checkBypass(url.parse('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) - }) -}) +import * as http from "http"; +import * as httpm from "../_out"; +import * as pm from "../_out/proxy"; +import * as proxy from "proxy"; +import * as url from "url"; + +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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("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(url.parse("https://github.com")); + expect(bypass).toBeFalsy(); + }); + + it("checkBypass returns false if empty no_proxy", () => { + process.env["no_proxy"] = ""; + let bypass = pm.checkBypass(url.parse("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); + }); +}); 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; -} \ No newline at end of file + 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; +} diff --git a/auth.ts b/auth.ts index ae00211..c8063bc 100644 --- a/auth.ts +++ b/auth.ts @@ -1,71 +1,86 @@ - -import ifm = require('./interfaces'); +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 { - return null; - } + 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 { + 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 { - return null; - } + 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 { + 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 { - 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 { + return null; + } } diff --git a/index.ts b/index.ts index e6a74c2..4bab312 100644 --- a/index.ts +++ b/index.ts @@ -1,48 +1,48 @@ import url = require("url"); import http = require("http"); import https = require("https"); -import ifm = require('./interfaces'); -import pm = require('./proxy'); +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, + 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 Headers { + Accept = "accept", + ContentType = "content-type", } export enum MediaTypes { - ApplicationJson = "application/json" + ApplicationJson = "application/json", } /** @@ -50,553 +50,702 @@ export enum MediaTypes { * @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(url.parse(serverUrl)) - return proxyUrl ? proxyUrl.href : '' + let proxyUrl = pm.getProxyUrl(url.parse(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 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 { - return new Promise(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()); - }); - }); - } + constructor(message: http.IncomingMessage) { + this.message = message; + } + + public message: http.IncomingMessage; + readBody(): Promise { + return new Promise(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:'; + let parsedUrl: url.Url = url.parse(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; - } - } + 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 { - return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + } + + public options( + requestUrl: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("OPTIONS", requestUrl, null, additionalHeaders || {}); + } + + public get( + requestUrl: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("GET", requestUrl, null, additionalHeaders || {}); + } + + public del( + requestUrl: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("DELETE", requestUrl, null, additionalHeaders || {}); + } + + public post( + requestUrl: string, + data: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("POST", requestUrl, data, additionalHeaders || {}); + } + + public patch( + requestUrl: string, + data: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("PATCH", requestUrl, data, additionalHeaders || {}); + } + + public put( + requestUrl: string, + data: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("PUT", requestUrl, data, additionalHeaders || {}); + } + + public head( + requestUrl: string, + additionalHeaders?: ifm.IHeaders + ): Promise { + return this.request("HEAD", requestUrl, null, additionalHeaders || {}); + } + + public sendStream( + verb: string, + requestUrl: string, + stream: NodeJS.ReadableStream, + additionalHeaders?: ifm.IHeaders + ): Promise { + 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( + requestUrl: string, + additionalHeaders: ifm.IHeaders = {} + ): Promise> { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.Accept, + MediaTypes.ApplicationJson + ); + let res: ifm.IHttpClientResponse = await this.get( + requestUrl, + additionalHeaders + ); + return this._processResponse(res, this.requestOptions); + } + + public async postJson( + requestUrl: string, + obj: any, + additionalHeaders: ifm.IHeaders = {} + ): Promise> { + 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(res, this.requestOptions); + } + + public async putJson( + requestUrl: string, + obj: any, + additionalHeaders: ifm.IHeaders = {} + ): Promise> { + 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(res, this.requestOptions); + } + + public async patchJson( + requestUrl: string, + obj: any, + additionalHeaders: ifm.IHeaders = {} + ): Promise> { + 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(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 { + if (this._disposed) { + throw new Error("Client has already been disposed."); } - public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('GET', requestUrl, null, additionalHeaders || {}); - } + 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; + } + } - public del(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('DELETE', requestUrl, null, additionalHeaders || {}); - } + 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." + ); + } - public post(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('POST', requestUrl, data, additionalHeaders || {}); - } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + await response.readBody(); - public patch(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('PATCH', requestUrl, data, additionalHeaders || {}); - } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = await this.requestRaw(info, data); + redirectsRemaining--; + } - public put(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('PUT', requestUrl, data, additionalHeaders || {}); - } + if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { + // If not a retry code, return immediately instead of retrying + return response; + } - public head(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('HEAD', requestUrl, null, additionalHeaders || {}); - } + numTries += 1; - public sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: ifm.IHeaders): Promise { - return this.request(verb, requestUrl, stream, additionalHeaders); + if (numTries < maxTries) { + await response.readBody(); + await this._performExponentialBackoff(numTries); + } } - /** - * 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(requestUrl: string, additionalHeaders: ifm.IHeaders = {}): Promise> { - additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson) - let res: ifm.IHttpClientResponse = await this.get(requestUrl, additionalHeaders); - return this._processResponse(res, this.requestOptions); - } - - public async postJson(requestUrl: string, obj: any, additionalHeaders: ifm.IHeaders = {}): Promise> { - 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(res, this.requestOptions); - } - - public async putJson(requestUrl: string, obj: any, additionalHeaders: ifm.IHeaders = {}): Promise> { - 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(res, this.requestOptions); - } - - public async patchJson(requestUrl: string, obj: any, additionalHeaders: ifm.IHeaders = {}): Promise> { - 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(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 { - 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; + } - return response; + /** + * Needs to be called if keepAlive is set to true in request options. + */ + public dispose() { + if (this._agent) { + this._agent.destroy(); } - /** - * 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 { + return new Promise((resolve, reject) => { + let callbackForResult = function ( + err: any, + res: ifm.IHttpClientResponse + ) { + if (err) { + reject(err); } - - this._disposed = true; + + 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"); } - /** - * Raw request. - * @param info - * @param data - */ - public requestRaw(info: ifm.IRequestInfo, data: string | NodeJS.ReadableStream): Promise { - return new Promise((resolve, reject) => { - let callbackForResult = function (err: any, res: ifm.IHttpClientResponse) { - if (err) { - reject(err); - } - - resolve(res); - }; - - this.requestRawWithCallback(info, data, callbackForResult); - }); + 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"); } - /** - * 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'); - } + if (data && typeof data !== "string") { + data.on("close", function () { + req.end(); + }); - 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'); - } + 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 = url.parse(serverUrl); + return this._getAgent(parsedUrl); + } + + private _prepareRequest( + method: string, + requestUrl: url.Url, + headers: ifm.IHeaders + ): ifm.IRequestInfo { + const info: 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 = {}; + 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; + } - if (data && typeof (data) !== 'string') { - data.on('close', function () { - req.end(); - }); + info.options.agent = this._getAgent(info.parsedUrl); - data.pipe(req); - } - else { - req.end(); - } + // gives handlers an opportunity to participate + if (this.handlers) { + this.handlers.forEach((handler) => { + handler.prepareRequest(info.options); + }); } - /** - * 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 = url.parse(serverUrl) - return this._getAgent(parsedUrl) - } + return info; + } - private _prepareRequest(method: string, requestUrl: url.Url, headers: ifm.IHeaders): ifm.IRequestInfo { - const info: 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 = {}; - 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); - }); - } + private _mergeHeaders(headers: ifm.IHeaders): ifm.IHeaders { + const lowercaseKeys = (obj) => + Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); - return info; + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign( + {}, + lowercaseKeys(this.requestOptions.headers), + lowercaseKeys(headers) + ); } - private _mergeHeaders(headers: ifm.IHeaders) : ifm.IHeaders { - const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {}); + return lowercaseKeys(headers || {}); + } - if (this.requestOptions && this.requestOptions.headers) { - return Object.assign( - {}, - lowercaseKeys(this.requestOptions.headers), - 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), {}); - return lowercaseKeys(headers || {}); + let clientHeader: string; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; } + return additionalHeaders[header] || clientHeader || _default; + } - private _getExistingOrDefaultHeader(additionalHeaders: ifm.IHeaders, header: string, _default: string) { - const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {}); + private _getAgent(parsedUrl: url.Url): http.Agent { + let agent; + let proxyUrl: url.Url = pm.getProxyUrl(parsedUrl); + let useProxy = proxyUrl && proxyUrl.hostname; - let clientHeader: string; - if(this.requestOptions && this.requestOptions.headers) { - clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; - } - return additionalHeaders[header] || clientHeader || _default; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; } - private _getAgent(parsedUrl: url.Url): http.Agent { - let agent; - let proxyUrl: url.Url = pm.getProxyUrl(parsedUrl); - let useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && !useProxy) { + agent = this._agent; + } - if (this._keepAlive && useProxy) { - agent = this._proxyAgent; - } + // if agent is already assigned use that agent. + if (!!agent) { + return agent; + } - if (this._keepAlive && !useProxy) { - agent = this._agent; - } + const usingSsl = parsedUrl.protocol === "https:"; + let maxSockets = 100; + if (!!this.requestOptions) { + maxSockets = + this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } - // if agent is already assigned use that agent. - if (!!agent) { - return agent; - } + 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; + } - const usingSsl = parsedUrl.protocol === 'https:'; - let maxSockets = 100; - if (!!this.requestOptions) { - maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets - } + // 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 (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 not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } - // 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 (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, + }); + } - // if not using private agent and tunnel agent isn't setup then use global agent - if (!agent) { - agent = usingSsl ? https.globalAgent : http.globalAgent; + return agent; + } + + private _performExponentialBackoff(retryNumber: number): Promise { + 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( + res: ifm.IHttpClientResponse, + options: ifm.IRequestOptions + ): Promise> { + return new Promise>(async (resolve, reject) => { + const statusCode: number = res.message.statusCode; + + const response: ifm.ITypedResponse = { + 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; } - 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 }); + 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 + ")"; } - return agent; - } + let err: Error = new Error(msg); - private _performExponentialBackoff(retryNumber: number): Promise { - 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; - } + // attach statusCode and body obj (if available) to the error object + err["statusCode"] = statusCode; + if (response.result) { + err["result"] = response.result; } - return value; - } - - private async _processResponse(res: ifm.IHttpClientResponse, options: ifm.IRequestOptions): Promise> { - return new Promise>(async (resolve, reject) => { - const statusCode: number = res.message.statusCode; - - const response: ifm.ITypedResponse = { - 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: Error = new Error(msg); - - // attach statusCode and body obj (if available) to the error object - err['statusCode'] = statusCode; - if (response.result) { - err['result'] = response.result; - } - - reject(err); - } else { - resolve(response); - } - }); - } + reject(err); + } else { + resolve(response); + } + }); + } } diff --git a/interfaces.ts b/interfaces.ts index 733d76b..0a50af8 100644 --- a/interfaces.ts +++ b/interfaces.ts @@ -1,55 +1,99 @@ import http = require("http"); import url = require("url"); -export interface IHeaders { [key: string]: any }; +export interface IHeaders { + [key: string]: any; +} export interface IHttpClient { - options(requestUrl: string, additionalHeaders?: IHeaders): Promise; - get(requestUrl: string, additionalHeaders?: IHeaders): Promise; - del(requestUrl: string, additionalHeaders?: IHeaders): Promise; - post(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise; - patch(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise; - put(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise; - sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: IHeaders): Promise; - request(verb: string, requestUrl: string, data: string | NodeJS.ReadableStream, headers: IHeaders): Promise; - requestRaw(info: IRequestInfo, data: string | NodeJS.ReadableStream): Promise; - requestRawWithCallback(info: IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: IHttpClientResponse) => void): void; + options( + requestUrl: string, + additionalHeaders?: IHeaders + ): Promise; + get( + requestUrl: string, + additionalHeaders?: IHeaders + ): Promise; + del( + requestUrl: string, + additionalHeaders?: IHeaders + ): Promise; + post( + requestUrl: string, + data: string, + additionalHeaders?: IHeaders + ): Promise; + patch( + requestUrl: string, + data: string, + additionalHeaders?: IHeaders + ): Promise; + put( + requestUrl: string, + data: string, + additionalHeaders?: IHeaders + ): Promise; + sendStream( + verb: string, + requestUrl: string, + stream: NodeJS.ReadableStream, + additionalHeaders?: IHeaders + ): Promise; + request( + verb: string, + requestUrl: string, + data: string | NodeJS.ReadableStream, + headers: IHeaders + ): Promise; + requestRaw( + info: IRequestInfo, + data: string | NodeJS.ReadableStream + ): Promise; + 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; + prepareRequest(options: http.RequestOptions): void; + canHandleAuthentication(response: IHttpClientResponse): boolean; + handleAuthentication( + httpClient: IHttpClient, + requestInfo: IRequestInfo, + objs + ): Promise; } export interface IHttpClientResponse { - message: http.IncomingMessage; - readBody(): Promise; + message: http.IncomingMessage; + readBody(): Promise; } export interface IRequestInfo { - options: http.RequestOptions; - parsedUrl: url.Url; - httpModule: any; + 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; - deserializeDates?: boolean; - // Allows retries only on Read operations (since writes may not be idempotent) - allowRetries?: boolean; - maxRetries?: number; + 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 { - statusCode: number, - result: T | null, - headers: Object + statusCode: number; + result: T | null; + headers: Object; } diff --git a/jest.config.js b/jest.config.js index 82fc98f..c3dd2b1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,10 @@ module.exports = { clearMocks: true, - moduleFileExtensions: ['js', 'ts'], - testEnvironment: 'node', - testMatch: ['**/__tests__/*.test.ts'], + moduleFileExtensions: ["js", "ts"], + testEnvironment: "node", + testMatch: ["**/__tests__/*.test.ts"], transform: { - '^.+\\.ts$': 'ts-jest' + "^.+\\.ts$": "ts-jest", }, - verbose: true -} \ No newline at end of file + verbose: true, +}; diff --git a/package-lock.json b/package-lock.json index 0369352..25a3f03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3413,6 +3413,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.4.tgz", + "integrity": "sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==", + "dev": true + }, "pretty-format": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", diff --git a/package.json b/package.json index 3fce213..a5ec27d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "main": "index.js", "scripts": { "build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out", + "format": "prettier --write **/*.{ts,js,json,yml}", + "format-check": "prettier --check **/*.{ts,js,json,yml}", "test": "jest" }, "repository": { @@ -25,6 +27,7 @@ "@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" diff --git a/proxy.ts b/proxy.ts index 76c59b0..159aa89 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,65 +1,62 @@ -import * as url from 'url'; +import * as url from "url"; export function getProxyUrl(reqUrl: url.Url): url.Url | undefined { - let usingSsl = reqUrl.protocol === 'https:'; + let usingSsl = reqUrl.protocol === "https:"; - let proxyUrl: url.Url; - if (checkBypass(reqUrl)) { - return proxyUrl; - } + let proxyUrl: url.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"]; - } + 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); - } + if (proxyVar) { + proxyUrl = url.parse(proxyVar); + } - return proxyUrl; + return proxyUrl; } - export function checkBypass(reqUrl: url.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 -} \ No newline at end of file + 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; +} diff --git a/tsconfig.json b/tsconfig.json index 560cca0..e96f704 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,12 @@ { - "compilerOptions": { - "target": "es2019", - "module": "commonjs", - "moduleResolution": "node", - "typeRoots": [ "node_modules/@types" ], - "declaration": true, - "outDir": "_out", - "forceConsistentCasingInFileNames": true - }, - "files": [ - "index.ts", - "auth.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "moduleResolution": "node", + "typeRoots": ["node_modules/@types"], + "declaration": true, + "outDir": "_out", + "forceConsistentCasingInFileNames": true + }, + "files": ["index.ts", "auth.ts"] +}