diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e716834..100b43b 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: npm test - run: npm test + - name: Lint + run: npm run lint - - name: audit security - run: npm audit + - name: npm test + run: npm test + + - name: audit security + run: npm audit 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; + username: string; + password: string; - constructor(username: string, password: string) { - this.username = username; - this.password = password; - } + 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'); - } + 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; - } + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } + handleAuthentication( + httpClient: ifm.IHttpClient, + requestInfo: ifm.IRequestInfo, + objs + ): Promise { + return null; + } } export class BearerCredentialHandler implements ifm.IRequestHandler { - token: string; + token: string; - constructor(token: string) { - this.token = token; - } + 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; - } + // 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; - } + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } + handleAuthentication( + httpClient: ifm.IHttpClient, + requestInfo: ifm.IRequestInfo, + objs + ): Promise { + return null; + } } -export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { - token: string; +export class PersonalAccessTokenCredentialHandler + implements ifm.IRequestHandler { + token: string; - constructor(token: string) { - this.token = token; - } + 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'); - } + // 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; - } + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } + 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; - } + constructor(message: http.IncomingMessage) { + this.message = message; + } - public message: http.IncomingMessage; - readBody(): Promise { - return new Promise(async (resolve, reject) => { - let output = Buffer.alloc(0); + 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("data", (chunk: Buffer) => { + output = Buffer.concat([output, chunk]); + }); - this.message.on('end', () => { - resolve(output.toString()); - }); - }); - } + 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; + 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; + 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; - } + 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; + this._socketTimeout = requestOptions.socketTimeout; - if (requestOptions.allowRedirects != null) { - this._allowRedirects = requestOptions.allowRedirects; - } + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } - if (requestOptions.allowRedirectDowngrade != null) { - this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; - } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } - if (requestOptions.maxRedirects != null) { - this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); - } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } - if (requestOptions.keepAlive != null) { - this._keepAlive = requestOptions.keepAlive; - } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } - if (requestOptions.allowRetries != null) { - this._allowRetries = requestOptions.allowRetries; - } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } - if (requestOptions.maxRetries != null) { - this._maxRetries = requestOptions.maxRetries; - } - } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + + 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 options(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); - } + let parsedUrl = url.parse(requestUrl); + let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers); - public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('GET', requestUrl, null, additionalHeaders || {}); - } + // 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; - public del(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('DELETE', requestUrl, null, additionalHeaders || {}); - } + let response: HttpClientResponse; + while (numTries < maxTries) { + response = await this.requestRaw(info, data); - public post(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise { - return this.request('POST', requestUrl, data, additionalHeaders || {}); - } + // Check if it's an authentication challenge + if ( + response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized + ) { + let authenticationHandler: ifm.IRequestHandler; - 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."); + for (let i = 0; i < this.handlers.length; i++) { + if (this.handlers[i].canHandleAuthentication(response)) { + authenticationHandler = this.handlers[i]; + break; + } } - let parsedUrl = url.parse(requestUrl); - let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers); + 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; + } + } - // 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); - } + 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); + } } - /** - * Needs to be called if keepAlive is set to true in request options. - */ - public dispose() { - if (this._agent) { - this._agent.destroy(); - } - - this._disposed = true; + return response; + } + + /** + * Needs to be called if keepAlive is set to true in request options. + */ + public dispose() { + if (this._agent) { + this._agent.destroy(); } - /** - * 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); - }; + /** + * 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.requestRawWithCallback(info, data, callbackForResult); - }); + 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 with callback. - * @param info - * @param data - * @param onResult - */ - public requestRawWithCallback(info: ifm.IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: ifm.IHttpClientResponse) => void): void { - let socket; + let callbackCalled: boolean = false; + let handleResult = (err: any, res: HttpClientResponse) => { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + }; - if (typeof (data) === 'string') { - info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8'); - } + let req: http.ClientRequest = info.httpModule.request( + info.options, + (msg: http.IncomingMessage) => { + let res: HttpClientResponse = new HttpClientResponse(msg); + handleResult(null, res); + } + ); - let callbackCalled: boolean = false; - let handleResult = (err: any, res: HttpClientResponse) => { - if (!callbackCalled) { - callbackCalled = true; - onResult(err, res); - } - }; + req.on("socket", (sock) => { + socket = sock; + }); - let req: http.ClientRequest = info.httpModule.request(info.options, (msg: http.IncomingMessage) => { - let res: HttpClientResponse = new HttpClientResponse(msg); - handleResult(null, res); - }); + // 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('socket', (sock) => { - socket = sock; - }); + req.on("error", function (err) { + // err has statusCode property + // res should have headers + handleResult(err, null); + }); - // If we ever get disconnected, we want the socket to timeout eventually - req.setTimeout(this._socketTimeout || 3 * 60000, () => { - if (socket) { - socket.end(); - } - handleResult(new Error('Request timeout: ' + info.options.path), null); - }); - - req.on('error', function (err) { - // err has statusCode property - // res should have headers - handleResult(err, null); - }); - - if (data && typeof (data) === 'string') { - req.write(data, 'utf8'); - } - - if (data && typeof (data) !== 'string') { - data.on('close', function () { - req.end(); - }); - - data.pipe(req); - } - else { - req.end(); - } + if (data && typeof data === "string") { + req.write(data, "utf8"); } - /** - * 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) + if (data && typeof data !== "string") { + data.on("close", function () { + req.end(); + }); + + data.pipe(req); + } else { + req.end(); + } + } + + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + public getAgent(serverUrl: string): http.Agent { + let parsedUrl = 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; } - private _prepareRequest(method: string, requestUrl: url.Url, headers: ifm.IHeaders): ifm.IRequestInfo { - const info: ifm.IRequestInfo = {}; + info.options.agent = this._getAgent(info.parsedUrl); - 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); - }); - } - - return info; + // 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), {}); + + let clientHeader: string; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + + 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._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._proxyAgent; - } - - if (this._keepAlive && !useProxy) { - agent = this._agent; - } - - // if agent is already assigned use that agent. - if (!!agent) { - return agent; - } - - const usingSsl = parsedUrl.protocol === 'https:'; - let maxSockets = 100; - if (!!this.requestOptions) { - maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets - } - - if (useProxy) { - // If using proxy, need tunnel - if (!tunnel) { - tunnel = require('tunnel'); - } - - const agentOptions = { - maxSockets: maxSockets, - keepAlive: this._keepAlive, - proxy: { - proxyAuth: proxyUrl.auth, - host: proxyUrl.hostname, - port: proxyUrl.port - }, - }; - - let tunnelAgent: Function; - const overHttps = proxyUrl.protocol === 'https:'; - if (usingSsl) { - tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; - } else { - tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; - } - - agent = tunnelAgent(agentOptions); - this._proxyAgent = agent; - } - - // if reusing agent across request and tunneling agent isn't assigned create a new agent - if (this._keepAlive && !agent) { - const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; - agent = usingSsl ? new https.Agent(options) : new http.Agent(options); - this._agent = agent; - } - - // if not using private agent and tunnel agent isn't setup then use global agent - if (!agent) { - agent = usingSsl ? https.globalAgent : http.globalAgent; - } - - if (usingSsl && this._ignoreSslError) { - // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process - // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options - // we have to cast it to any and change it directly - agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false }); - } - - return agent; + if (this._keepAlive && !useProxy) { + agent = this._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)); + // if agent is already assigned use that agent. + if (!!agent) { + return agent; } - private static dateTimeDeserializer(key: any, value: any): any { - if (typeof value === 'string'){ - let a = new Date(value); - if (!isNaN(a.valueOf())) { - return a; - } + const usingSsl = parsedUrl.protocol === "https:"; + let maxSockets = 100; + if (!!this.requestOptions) { + maxSockets = + this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + + if (useProxy) { + // If using proxy, need tunnel + if (!tunnel) { + tunnel = require("tunnel"); + } + + const agentOptions = { + maxSockets: maxSockets, + keepAlive: this._keepAlive, + proxy: { + proxyAuth: proxyUrl.auth, + host: proxyUrl.hostname, + port: proxyUrl.port, + }, + }; + + let tunnelAgent: Function; + const overHttps = proxyUrl.protocol === "https:"; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } + + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false, + }); + } + + return agent; + } + + private _performExponentialBackoff(retryNumber: number): Promise { + 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; } - return value; - } + response.headers = res.message.headers; + } catch (err) { + // Invalid resource (contents not json); leaving result obj null + } - private async _processResponse(res: ifm.IHttpClientResponse, options: ifm.IRequestOptions): Promise> { - return new Promise>(async (resolve, reject) => { - const statusCode: number = res.message.statusCode; + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg: string; - const response: ifm.ITypedResponse = { - statusCode: statusCode, - result: null, - headers: {} - }; + // 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 + ")"; + } - // not found leads to null obj returned - if (statusCode == HttpCodes.NotFound) { - resolve(response); - } + let err: Error = new Error(msg); - let obj: any; - let contents: string; + // attach statusCode and body obj (if available) to the error object + err["statusCode"] = statusCode; + if (response.result) { + err["result"] = response.result; + } - // 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..edea80e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out", + "lint": "prettier --check *.{ts,js,json} **/*.yml", "test": "jest" }, "repository": { @@ -25,6 +26,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 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"]; - } - - if (proxyVar) { - proxyUrl = url.parse(proxyVar); - } + let usingSsl = reqUrl.protocol === "https:"; + 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"]; + } + + if (proxyVar) { + proxyUrl = url.parse(proxyVar); + } + + return proxyUrl; } - export function checkBypass(reqUrl: url.Url): boolean { - if (!reqUrl.hostname) { - return false - } + if (!reqUrl.hostname) { + return false; + } - let noProxy = process.env["no_proxy"] || process.env["NO_PROXY"] || ''; - if (!noProxy) { - 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 - } + // 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}`) - } + // 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 - } + // 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 + 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"] +}