From 7d4c99a232a40867555b083de8501a0f1373941d Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 6 Sep 2018 14:21:00 -0400 Subject: [PATCH 1/9] Upgrade to latest typed-rest-client --- api/handlers/basiccreds.ts | 7 +++-- api/handlers/bearertoken.ts | 7 +++-- api/handlers/ntlm.ts | 22 +++++++++------ api/handlers/personalaccesstoken.ts | 7 +++-- api/interfaces/common/VsoBaseInterfaces.ts | 33 ++++++++++++++++++---- package-lock.json | 6 ++-- package.json | 2 +- 7 files changed, 59 insertions(+), 25 deletions(-) diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts index d15bff59..dd7e47ba 100644 --- a/api/handlers/basiccreds.ts +++ b/api/handlers/basiccreds.ts @@ -20,10 +20,13 @@ export class BasicCredentialHandler implements VsoBaseInterfaces.IRequestHandler } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean { + canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + return new Promise(async (resolve, reject) => { + resolve(null); + }); } } diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts index 8e3e5c5b..ee4ad976 100644 --- a/api/handlers/bearertoken.ts +++ b/api/handlers/bearertoken.ts @@ -18,10 +18,13 @@ export class BearerCredentialHandler implements VsoBaseInterfaces.IRequestHandle } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean { + canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + return new Promise(async (resolve, reject) => { + resolve(null); + }); } } diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts index cf037d8b..fa90a489 100644 --- a/api/handlers/ntlm.ts +++ b/api/handlers/ntlm.ts @@ -33,11 +33,11 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler } } - canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean { - if (res && res.statusCode === 401) { + canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { + if (res && res.message.statusCode === 401) { // Ensure that we're talking NTLM here // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM - var wwwAuthenticate = res.headers['www-authenticate']; + var wwwAuthenticate = res.message.headers['www-authenticate']; if (wwwAuthenticate !== undefined) { var mechanisms = wwwAuthenticate.split(', '); var idx = mechanisms.indexOf("NTLM"); @@ -54,16 +54,17 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + //handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { // Set up the headers for NTLM authentication - var ntlmOptions = _.extend(options, { + var ntlmOptions = _.extend(requestInfo.options, { username: this.username, password: this.password, domain: this.domain || '', workstation: this.workstation || '' }); var keepaliveAgent; - if (httpClient.isSsl === true) { + if(requestInfo.options.protocol == "https") { keepaliveAgent = new https.Agent({}); } else { keepaliveAgent = new http.Agent({ keepAlive: true }); @@ -72,14 +73,17 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler // The following pattern of sending the type1 message following immediately (in a setImmediate) is // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) // the NTLM exchange will always fail with a 401. - this.sendType1Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { + this.sendType1Message(httpClient, requestInfo.options.protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { if (err) { - return finalCallback(err, null, null); + return objs.finalCallback(err, null, null); } setImmediate(function () { - self.sendType3Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, res, finalCallback); + self.sendType3Message(httpClient, requestInfo.options.protocol, ntlmOptions, objs, keepaliveAgent, res, objs.finalCallback); }); }); + return new Promise(async (resolve, reject) => { + resolve(null); + }); } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts index fd6ba616..936fadae 100644 --- a/api/handlers/personalaccesstoken.ts +++ b/api/handlers/personalaccesstoken.ts @@ -18,10 +18,13 @@ export class PersonalAccessTokenCredentialHandler implements VsoBaseInterfaces.I } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean { + canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + return new Promise(async (resolve, reject) => { + resolve(null); + }); } } diff --git a/api/interfaces/common/VsoBaseInterfaces.ts b/api/interfaces/common/VsoBaseInterfaces.ts index 05a5fe91..6199efdb 100644 --- a/api/interfaces/common/VsoBaseInterfaces.ts +++ b/api/interfaces/common/VsoBaseInterfaces.ts @@ -6,6 +6,8 @@ //---------------------------------------------------------------------------- import Serialization = require('../../Serialization'); +import http = require("http"); +import url = require("url"); /** * Information about the location of a REST API resource @@ -52,15 +54,34 @@ export interface IBasicCredentials { password: string; } +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; +} + +export interface IRequestInfo { + options: http.RequestOptions; + parsedUrl: url.Url; + httpModule: any; +} + export interface IRequestHandler { - prepareRequest(options: any): void; - canHandleAuthentication(res: IHttpResponse): boolean; - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void; + prepareRequest(options: http.RequestOptions): void; + canHandleAuthentication(response: IHttpClientResponse): boolean; + handleAuthentication(httpClient: IHttpClient, requestInfo: IRequestInfo, objs): Promise; } -export interface IHttpResponse { - statusCode?: number; - headers: any; +export interface IHttpClientResponse { + message: http.IncomingMessage; + readBody(): Promise; } export interface IRequestOptions { diff --git a/package-lock.json b/package-lock.json index d1ca10b9..1513d719 100644 --- a/package-lock.json +++ b/package-lock.json @@ -172,9 +172,9 @@ "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" }, "typed-rest-client": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-0.12.0.tgz", - "integrity": "sha1-Y3b1Un9CfaEh3K/f1+QeEyHgcgw=", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.0.9.tgz", + "integrity": "sha512-iOdwgmnP/tF6Qs+oY4iEtCf/3fnCDl7Gy9LGPJ4E3M4Wj3uaSko15FVwbsaBmnBqTJORnXBWVY5306D4HH8oiA==", "requires": { "tunnel": "0.0.4", "underscore": "1.8.3" diff --git a/package.json b/package.json index cffb41ff..e82d5f60 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "license": "MIT", "dependencies": { "tunnel": "0.0.4", - "typed-rest-client": "^0.12.0", + "typed-rest-client": "1.0.9", "underscore": "1.8.3" }, "devDependencies": { From ff8ad0fa8ce8e6d3eceb3a75bcae28542239c1c4 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 6 Sep 2018 14:22:09 -0400 Subject: [PATCH 2/9] Missed 2 files --- samples/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/package-lock.json b/samples/package-lock.json index 47fad46f..da1b04a5 100644 --- a/samples/package-lock.json +++ b/samples/package-lock.json @@ -8,7 +8,7 @@ "version": "file:../_build", "requires": { "tunnel": "0.0.4", - "typed-rest-client": "0.12.0", + "typed-rest-client": "1.0.9", "underscore": "1.8.3" }, "dependencies": { @@ -17,7 +17,7 @@ "bundled": true }, "typed-rest-client": { - "version": "0.12.0", + "version": "1.0.9", "bundled": true, "requires": { "tunnel": "0.0.4", From aa5b372c112917f488f5cce9c808af3cc9e77e96 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 6 Sep 2018 15:11:55 -0400 Subject: [PATCH 3/9] Was misusing some variables, had to do away with final callback option --- api/handlers/basiccreds.ts | 4 +--- api/handlers/bearertoken.ts | 4 +--- api/handlers/ntlm.ts | 19 ++++++++----------- api/handlers/personalaccesstoken.ts | 4 +--- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts index dd7e47ba..3d6bd38d 100644 --- a/api/handlers/basiccreds.ts +++ b/api/handlers/basiccreds.ts @@ -25,8 +25,6 @@ export class BasicCredentialHandler implements VsoBaseInterfaces.IRequestHandler } handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { - return new Promise(async (resolve, reject) => { - resolve(null); - }); + return null; } } diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts index ee4ad976..7f77bb0e 100644 --- a/api/handlers/bearertoken.ts +++ b/api/handlers/bearertoken.ts @@ -23,8 +23,6 @@ export class BearerCredentialHandler implements VsoBaseInterfaces.IRequestHandle } handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { - return new Promise(async (resolve, reject) => { - resolve(null); - }); + return null; } } diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts index fa90a489..334ce8f6 100644 --- a/api/handlers/ntlm.ts +++ b/api/handlers/ntlm.ts @@ -55,7 +55,6 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { - //handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { // Set up the headers for NTLM authentication var ntlmOptions = _.extend(requestInfo.options, { username: this.username, @@ -64,7 +63,7 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler workstation: this.workstation || '' }); var keepaliveAgent; - if(requestInfo.options.protocol == "https") { + if(requestInfo.httpModule == https) { keepaliveAgent = new https.Agent({}); } else { keepaliveAgent = new http.Agent({ keepAlive: true }); @@ -73,17 +72,15 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler // The following pattern of sending the type1 message following immediately (in a setImmediate) is // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) // the NTLM exchange will always fail with a 401. - this.sendType1Message(httpClient, requestInfo.options.protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { + this.sendType1Message(httpClient, requestInfo.parsedUrl.protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { if (err) { - return objs.finalCallback(err, null, null); + throw err; } setImmediate(function () { - self.sendType3Message(httpClient, requestInfo.options.protocol, ntlmOptions, objs, keepaliveAgent, res, objs.finalCallback); + self.sendType3Message(httpClient, requestInfo.parsedUrl.protocol, ntlmOptions, objs, keepaliveAgent, res); }); }); - return new Promise(async (resolve, reject) => { - resolve(null); - }); + return null; } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js @@ -104,9 +101,9 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType3Message(httpClient, protocol, options, objs, keepaliveAgent, res, callback): void { + private sendType3Message(httpClient, protocol, options, objs, keepaliveAgent, res): void { if (!res.headers['www-authenticate']) { - return callback(new Error('www-authenticate not found on response of second request')); + throw new Error('www-authenticate not found on response of second request'); } // parse type2 message from server: var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']); @@ -124,6 +121,6 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler type3options.headers = _.extend(type3options.headers, options.headers); type3options = _.extend(type3options, _.omit(options, 'headers')); // send type3 message to server: - httpClient.requestInternal(protocol, type3options, objs, callback); + httpClient.requestInternal(protocol, type3options, objs); } } diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts index 936fadae..26cc1c90 100644 --- a/api/handlers/personalaccesstoken.ts +++ b/api/handlers/personalaccesstoken.ts @@ -23,8 +23,6 @@ export class PersonalAccessTokenCredentialHandler implements VsoBaseInterfaces.I } handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { - return new Promise(async (resolve, reject) => { - resolve(null); - }); + return null; } } From b9a2dcddbe0b5821951c31d4c67c997679faaaf8 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Fri, 7 Sep 2018 14:26:58 -0400 Subject: [PATCH 4/9] Updated VSO Client to serialize nested objects correctly --- api/VsoClient.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/api/VsoClient.ts b/api/VsoClient.ts index fc2e50d4..46b564f4 100644 --- a/api/VsoClient.ts +++ b/api/VsoClient.ts @@ -183,25 +183,40 @@ export class VsoClient { return url.resolve(this.baseUrl, path.join(this.basePath, relativeUrl)); } - private getSerializedObject(object: any): string { + private getSerializedObject(queryValue: any, object: any): string { let value:string = ""; let first:boolean = true; for (let property in object) { if (object.hasOwnProperty(property)) { let prop = object[property]; + let valueString = this.getValueString(property, prop); if (first && prop !== undefined) { - value += property + "=" + encodeURIComponent(prop); + value += property + "=" + valueString; first = false; } else if (prop !== undefined) { - value += "&" + property +"=" + encodeURIComponent(prop); + value += "&" + property +"=" + valueString; } } } + if (value == ""){ + value += queryValue + "=" + object.toString(); + } + return value; } + protected getValueString(queryValue, value) { + let valueString = null; + if (typeof(value) === 'object') { + valueString = this.getSerializedObject(queryValue, value); + } else { + valueString = queryValue + "=" + encodeURIComponent(value); + } + return valueString; + } + protected getRequestUrl(routeTemplate: string, area: string, resource: string, routeValues: any, queryParams?: any): string { // Add area/resource route values (based on the location) @@ -221,12 +236,7 @@ export class VsoClient { for (let queryValue in queryParams) { if (queryParams[queryValue] != null) { let value = queryParams[queryValue]; - let valueString = null; - if (typeof(value) === 'object') { - valueString = this.getSerializedObject(value); - } else { - valueString = queryValue + "=" + encodeURIComponent(queryParams[queryValue]); - } + let valueString = this.getValueString(queryValue, value); if (first) { relativeUrl += "?" + valueString; first = false; From b23584d1f6be942934c2b7269bf827e1407b3217 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Fri, 7 Sep 2018 15:13:17 -0400 Subject: [PATCH 5/9] Updated handlers to match typed-rest-client --- api/handlers/basiccreds.ts | 10 +- api/handlers/bearertoken.ts | 10 +- api/handlers/ntlm.ts | 174 +++++++++++++++++----------- api/handlers/personalaccesstoken.ts | 10 +- 4 files changed, 120 insertions(+), 84 deletions(-) diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts index 3d6bd38d..c615d1ba 100644 --- a/api/handlers/basiccreds.ts +++ b/api/handlers/basiccreds.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces'); +import ifm = require('../interfaces/common/VsoBaseInterfaces'); -export class BasicCredentialHandler implements VsoBaseInterfaces.IRequestHandler { +export class BasicCredentialHandler implements ifm.IRequestHandler { username: string; password: string; @@ -20,11 +20,11 @@ export class BasicCredentialHandler implements VsoBaseInterfaces.IRequestHandler } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { return null; } -} +} \ No newline at end of file diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts index 7f77bb0e..cf1388fb 100644 --- a/api/handlers/bearertoken.ts +++ b/api/handlers/bearertoken.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces'); +import ifm = require('../interfaces/common/VsoBaseInterfaces'); -export class BearerCredentialHandler implements VsoBaseInterfaces.IRequestHandler { +export class BearerCredentialHandler implements ifm.IRequestHandler { token: string; constructor(token: string) { @@ -18,11 +18,11 @@ export class BearerCredentialHandler implements VsoBaseInterfaces.IRequestHandle } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { return null; } -} +} \ No newline at end of file diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts index 334ce8f6..24563ca3 100644 --- a/api/handlers/ntlm.ts +++ b/api/handlers/ntlm.ts @@ -1,31 +1,38 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces'); - +import ifm = require('../interfaces/common/VsoBaseInterfaces'); import http = require("http"); import https = require("https"); + var _ = require("underscore"); var ntlm = require("../opensource/node-http-ntlm/ntlm"); -export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler { - username: string; - password: string; - workstation: string; - domain: string; +interface INtlmOptions { + username?: string, + password?: string, + domain: string, + workstation: string +} + +export class NtlmCredentialHandler implements ifm.IRequestHandler { + private _ntlmOptions: INtlmOptions; + + constructor(username: string, password: string, workstation?: string, domain?: string) { + this._ntlmOptions = {}; + + this._ntlmOptions.username = username; + this._ntlmOptions.password = password; - constructor(username: string, password: string, workstation?: string, domain?: string) { - this.username = username; - this.password = password; - if (workstation !== undefined) { - this.workstation = workstation; - } if (domain !== undefined) { - this.domain = domain; + this._ntlmOptions.domain = domain; + } + if (workstation !== undefined) { + this._ntlmOptions.workstation = workstation; } } - prepareRequest(options:any): void { + public prepareRequest(options: http.RequestOptions): void { // No headers or options need to be set. We keep the credentials on the handler itself. // If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time if (options.agent) { @@ -33,94 +40,123 @@ export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler } } - canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { - if (res && res.message.statusCode === 401) { + public canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + if (response && response.message && response.message.statusCode === 401) { // Ensure that we're talking NTLM here // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM - var wwwAuthenticate = res.message.headers['www-authenticate']; - if (wwwAuthenticate !== undefined) { - var mechanisms = wwwAuthenticate.split(', '); - var idx = mechanisms.indexOf("NTLM"); - if (idx >= 0) { - // Check specifically for 'NTLM' since www-authenticate header can also contain - // the Authorization value to use in the form of 'NTLM TlRMTVNT....AAAADw==' - if (mechanisms[idx].length == 4) { - return true; - } + const wwwAuthenticate = response.message.headers['www-authenticate']; + + if (wwwAuthenticate) { + const mechanisms = wwwAuthenticate.split(', '); + const index = mechanisms.indexOf("NTLM"); + if (index >= 0) { + return true; } } } + return false; } - // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + public handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return new Promise((resolve, reject) => { + var callbackForResult = function (err: any, res: ifm.IHttpClientResponse) { + if(err) { + reject(err); + } + // We have to readbody on the response before continuing otherwise there is a hang. + res.readBody().then(() => { + resolve(res); + }); + }; + + this.handleAuthenticationPrivate(httpClient, requestInfo, objs, callbackForResult); + }); + } + + private handleAuthenticationPrivate(httpClient: any, requestInfo: ifm.IRequestInfo, objs, finalCallback): void { // Set up the headers for NTLM authentication - var ntlmOptions = _.extend(requestInfo.options, { - username: this.username, - password: this.password, - domain: this.domain || '', - workstation: this.workstation || '' + requestInfo.options = _.extend(requestInfo.options, { + username: this._ntlmOptions.username, + password: this._ntlmOptions.password, + domain: this._ntlmOptions.domain, + workstation: this._ntlmOptions.workstation }); - var keepaliveAgent; - if(requestInfo.httpModule == https) { - keepaliveAgent = new https.Agent({}); + + if (httpClient.isSsl === true) { + requestInfo.options.agent = new https.Agent({ keepAlive: true }); } else { - keepaliveAgent = new http.Agent({ keepAlive: true }); + requestInfo.options.agent = new http.Agent({ keepAlive: true }); } + let self = this; + // The following pattern of sending the type1 message following immediately (in a setImmediate) is // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) // the NTLM exchange will always fail with a 401. - this.sendType1Message(httpClient, requestInfo.parsedUrl.protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { + this.sendType1Message(httpClient, requestInfo, objs, function (err, res) { if (err) { - throw err; + return finalCallback(err, null, null); } - setImmediate(function () { - self.sendType3Message(httpClient, requestInfo.parsedUrl.protocol, ntlmOptions, objs, keepaliveAgent, res); + + /// We have to readbody on the response before continuing otherwise there is a hang. + res.readBody().then(() => { + // It is critical that we have setImmediate here due to how connection requests are queued. + // If setImmediate is removed then the NTLM handshake will not work. + // setImmediate allows us to queue a second request on the same connection. If this second + // request is not queued on the connection when the first request finishes then node closes + // the connection. NTLM requires both requests to be on the same connection so we need this. + setImmediate(function () { + self.sendType3Message(httpClient, requestInfo, objs, res, finalCallback); + }); }); }); - return null; } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType1Message(httpClient, protocol, options, objs, keepaliveAgent, callback): void { - var type1msg = ntlm.createType1Message(options); - var type1options = { + private sendType1Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, finalCallback): void { + const type1msg = ntlm.createType1Message(this._ntlmOptions); + + const type1options: http.RequestOptions = { headers: { 'Connection': 'keep-alive', 'Authorization': type1msg }, - timeout: options.timeout || 0, - agent: keepaliveAgent, - // don't redirect because http could change to https which means we need to change the keepaliveAgent - allowRedirects: false + timeout: requestInfo.options.timeout || 0, + agent: requestInfo.httpModule, }; - type1options = _.extend(type1options, _.omit(options, 'headers')); - httpClient.requestInternal(protocol, type1options, objs, callback); + + const type1info = {}; + type1info.httpModule = requestInfo.httpModule; + type1info.parsedUrl = requestInfo.parsedUrl; + type1info.options = _.extend(type1options, _.omit(requestInfo.options, 'headers')); + + return httpClient.requestRawWithCallback(type1info, objs, finalCallback); } // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType3Message(httpClient, protocol, options, objs, keepaliveAgent, res): void { - if (!res.headers['www-authenticate']) { + private sendType3Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, res, callback): void { + if (!res.message.headers && !res.message.headers['www-authenticate']) { throw new Error('www-authenticate not found on response of second request'); } - // parse type2 message from server: - var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']); - // create type3 message: - var type3msg = ntlm.createType3Message(type2msg, options); - // build type3 request: - var type3options = { + + const type2msg = ntlm.parseType2Message(res.message.headers['www-authenticate']); + const type3msg = ntlm.createType3Message(type2msg, this._ntlmOptions); + + const type3options: http.RequestOptions = { headers: { - 'Authorization': type3msg + 'Authorization': type3msg, + 'Connection': 'Close' }, - allowRedirects: false, - agent: keepaliveAgent + agent: requestInfo.httpModule, }; - // pass along other options: - type3options.headers = _.extend(type3options.headers, options.headers); - type3options = _.extend(type3options, _.omit(options, 'headers')); - // send type3 message to server: - httpClient.requestInternal(protocol, type3options, objs); + + const type3info = {}; + type3info.httpModule = requestInfo.httpModule; + type3info.parsedUrl = requestInfo.parsedUrl; + type3options.headers = _.extend(type3options.headers, requestInfo.options.headers); + type3info.options = _.extend(type3options, _.omit(requestInfo.options, 'headers')); + + return httpClient.requestRawWithCallback(type3info, objs, callback); } -} +} \ No newline at end of file diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts index 26cc1c90..286347a9 100644 --- a/api/handlers/personalaccesstoken.ts +++ b/api/handlers/personalaccesstoken.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces'); +import ifm = require('../interfaces/common/VsoBaseInterfaces'); -export class PersonalAccessTokenCredentialHandler implements VsoBaseInterfaces.IRequestHandler { +export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { token: string; constructor(token: string) { @@ -18,11 +18,11 @@ export class PersonalAccessTokenCredentialHandler implements VsoBaseInterfaces.I } // This handler cannot handle 401 - canHandleAuthentication(res: VsoBaseInterfaces.IHttpClientResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient: VsoBaseInterfaces.IHttpClient, requestInfo: VsoBaseInterfaces.IRequestInfo, objs): Promise { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { return null; } -} +} \ No newline at end of file From 7ae7a55ab3507b07c9ee6f640c4454654b8f0242 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Mon, 10 Sep 2018 08:53:57 -0400 Subject: [PATCH 6/9] Now uses the handlers from typed-rest-client --- api/WebApi.ts | 13 +-- api/handlers/basiccreds.ts | 30 ------ api/handlers/bearertoken.ts | 28 ----- api/handlers/ntlm.ts | 162 ---------------------------- api/handlers/personalaccesstoken.ts | 28 ----- 5 files changed, 5 insertions(+), 256 deletions(-) delete mode 100644 api/handlers/basiccreds.ts delete mode 100644 api/handlers/bearertoken.ts delete mode 100644 api/handlers/ntlm.ts delete mode 100644 api/handlers/personalaccesstoken.ts diff --git a/api/WebApi.ts b/api/WebApi.ts index ace7472f..5f5cfa7d 100644 --- a/api/WebApi.ts +++ b/api/WebApi.ts @@ -26,11 +26,8 @@ import workm = require('./WorkApi'); import workitemtrackingm = require('./WorkItemTrackingApi'); import workitemtrackingprocessm = require('./WorkItemTrackingProcessApi'); import workitemtrackingprocessdefinitionm = require('./WorkItemTrackingProcessDefinitionsApi'); -import basicm = require('./handlers/basiccreds'); -import bearm = require('./handlers/bearertoken'); -import ntlmm = require('./handlers/ntlm'); -import patm = require('./handlers/personalaccesstoken'); +import * as resthandlers from 'typed-rest-client/Handlers'; import * as rm from 'typed-rest-client/RestClient'; import vsom = require('./VsoClient'); import lim = require("./interfaces/LocationsInterfaces"); @@ -43,19 +40,19 @@ import crypto = require('crypto'); */ export function getBasicHandler(username: string, password: string): VsoBaseInterfaces.IRequestHandler { - return new basicm.BasicCredentialHandler(username, password); + return new resthandlers.BasicCredentialHandler(username, password); } export function getNtlmHandler(username: string, password: string, workstation?: string, domain?: string): VsoBaseInterfaces.IRequestHandler { - return new ntlmm.NtlmCredentialHandler(username, password, workstation, domain); + return new resthandlers.NtlmCredentialHandler(username, password, workstation, domain); } export function getBearerHandler(token: string): VsoBaseInterfaces.IRequestHandler { - return new bearm.BearerCredentialHandler(token); + return new resthandlers.BearerCredentialHandler(token); } export function getPersonalAccessTokenHandler(token: string): VsoBaseInterfaces.IRequestHandler { - return new patm.PersonalAccessTokenCredentialHandler(token); + return new resthandlers.PersonalAccessTokenCredentialHandler(token); } export function getHandlerFromToken(token: string): VsoBaseInterfaces.IRequestHandler { diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts deleted file mode 100644 index c615d1ba..00000000 --- a/api/handlers/basiccreds.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import ifm = require('../interfaces/common/VsoBaseInterfaces'); - -export class BasicCredentialHandler implements ifm.IRequestHandler { - username: string; - password: string; - - constructor(username: string, password: string) { - this.username = username; - this.password = password; - } - - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options:any): void { - options.headers['Authorization'] = 'Basic ' + new Buffer(this.username + ':' + this.password).toString('base64'); - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } -} \ No newline at end of file diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts deleted file mode 100644 index cf1388fb..00000000 --- a/api/handlers/bearertoken.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import ifm = require('../interfaces/common/VsoBaseInterfaces'); - -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; - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } -} \ No newline at end of file diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts deleted file mode 100644 index 24563ca3..00000000 --- a/api/handlers/ntlm.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import ifm = require('../interfaces/common/VsoBaseInterfaces'); -import http = require("http"); -import https = require("https"); - -var _ = require("underscore"); -var ntlm = require("../opensource/node-http-ntlm/ntlm"); - -interface INtlmOptions { - username?: string, - password?: string, - domain: string, - workstation: string -} - -export class NtlmCredentialHandler implements ifm.IRequestHandler { - private _ntlmOptions: INtlmOptions; - - constructor(username: string, password: string, workstation?: string, domain?: string) { - this._ntlmOptions = {}; - - this._ntlmOptions.username = username; - this._ntlmOptions.password = password; - - if (domain !== undefined) { - this._ntlmOptions.domain = domain; - } - if (workstation !== undefined) { - this._ntlmOptions.workstation = workstation; - } - } - - public prepareRequest(options: http.RequestOptions): void { - // No headers or options need to be set. We keep the credentials on the handler itself. - // If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time - if (options.agent) { - delete options.agent; - } - } - - public canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - if (response && response.message && response.message.statusCode === 401) { - // Ensure that we're talking NTLM here - // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM - const wwwAuthenticate = response.message.headers['www-authenticate']; - - if (wwwAuthenticate) { - const mechanisms = wwwAuthenticate.split(', '); - const index = mechanisms.indexOf("NTLM"); - if (index >= 0) { - return true; - } - } - } - - return false; - } - - public handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return new Promise((resolve, reject) => { - var callbackForResult = function (err: any, res: ifm.IHttpClientResponse) { - if(err) { - reject(err); - } - // We have to readbody on the response before continuing otherwise there is a hang. - res.readBody().then(() => { - resolve(res); - }); - }; - - this.handleAuthenticationPrivate(httpClient, requestInfo, objs, callbackForResult); - }); - } - - private handleAuthenticationPrivate(httpClient: any, requestInfo: ifm.IRequestInfo, objs, finalCallback): void { - // Set up the headers for NTLM authentication - requestInfo.options = _.extend(requestInfo.options, { - username: this._ntlmOptions.username, - password: this._ntlmOptions.password, - domain: this._ntlmOptions.domain, - workstation: this._ntlmOptions.workstation - }); - - if (httpClient.isSsl === true) { - requestInfo.options.agent = new https.Agent({ keepAlive: true }); - } else { - requestInfo.options.agent = new http.Agent({ keepAlive: true }); - } - - let self = this; - - // The following pattern of sending the type1 message following immediately (in a setImmediate) is - // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) - // the NTLM exchange will always fail with a 401. - this.sendType1Message(httpClient, requestInfo, objs, function (err, res) { - if (err) { - return finalCallback(err, null, null); - } - - /// We have to readbody on the response before continuing otherwise there is a hang. - res.readBody().then(() => { - // It is critical that we have setImmediate here due to how connection requests are queued. - // If setImmediate is removed then the NTLM handshake will not work. - // setImmediate allows us to queue a second request on the same connection. If this second - // request is not queued on the connection when the first request finishes then node closes - // the connection. NTLM requires both requests to be on the same connection so we need this. - setImmediate(function () { - self.sendType3Message(httpClient, requestInfo, objs, res, finalCallback); - }); - }); - }); - } - - // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType1Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, finalCallback): void { - const type1msg = ntlm.createType1Message(this._ntlmOptions); - - const type1options: http.RequestOptions = { - headers: { - 'Connection': 'keep-alive', - 'Authorization': type1msg - }, - timeout: requestInfo.options.timeout || 0, - agent: requestInfo.httpModule, - }; - - const type1info = {}; - type1info.httpModule = requestInfo.httpModule; - type1info.parsedUrl = requestInfo.parsedUrl; - type1info.options = _.extend(type1options, _.omit(requestInfo.options, 'headers')); - - return httpClient.requestRawWithCallback(type1info, objs, finalCallback); - } - - // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType3Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, res, callback): void { - if (!res.message.headers && !res.message.headers['www-authenticate']) { - throw new Error('www-authenticate not found on response of second request'); - } - - const type2msg = ntlm.parseType2Message(res.message.headers['www-authenticate']); - const type3msg = ntlm.createType3Message(type2msg, this._ntlmOptions); - - const type3options: http.RequestOptions = { - headers: { - 'Authorization': type3msg, - 'Connection': 'Close' - }, - agent: requestInfo.httpModule, - }; - - const type3info = {}; - type3info.httpModule = requestInfo.httpModule; - type3info.parsedUrl = requestInfo.parsedUrl; - type3options.headers = _.extend(type3options.headers, requestInfo.options.headers); - type3info.options = _.extend(type3options, _.omit(requestInfo.options, 'headers')); - - return httpClient.requestRawWithCallback(type3info, objs, callback); - } -} \ No newline at end of file diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts deleted file mode 100644 index 286347a9..00000000 --- a/api/handlers/personalaccesstoken.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import ifm = require('../interfaces/common/VsoBaseInterfaces'); - -export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { - token: string; - - constructor(token: string) { - this.token = token; - } - - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options:any): void { - options.headers['Authorization'] = 'Basic ' + new Buffer('PAT:' + this.token).toString('base64'); - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; - } -} \ No newline at end of file From f3302dc664b65a1ac31e76d8d252b5b85a67bcbc Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Mon, 10 Sep 2018 14:51:58 -0400 Subject: [PATCH 7/9] Revert "Now uses the handlers from typed-rest-client" This reverts commit 7ae7a55ab3507b07c9ee6f640c4454654b8f0242. --- api/WebApi.ts | 13 ++- api/handlers/basiccreds.ts | 30 ++++++ api/handlers/bearertoken.ts | 28 +++++ api/handlers/ntlm.ts | 162 ++++++++++++++++++++++++++++ api/handlers/personalaccesstoken.ts | 28 +++++ 5 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 api/handlers/basiccreds.ts create mode 100644 api/handlers/bearertoken.ts create mode 100644 api/handlers/ntlm.ts create mode 100644 api/handlers/personalaccesstoken.ts diff --git a/api/WebApi.ts b/api/WebApi.ts index 5f5cfa7d..ace7472f 100644 --- a/api/WebApi.ts +++ b/api/WebApi.ts @@ -26,8 +26,11 @@ import workm = require('./WorkApi'); import workitemtrackingm = require('./WorkItemTrackingApi'); import workitemtrackingprocessm = require('./WorkItemTrackingProcessApi'); import workitemtrackingprocessdefinitionm = require('./WorkItemTrackingProcessDefinitionsApi'); +import basicm = require('./handlers/basiccreds'); +import bearm = require('./handlers/bearertoken'); +import ntlmm = require('./handlers/ntlm'); +import patm = require('./handlers/personalaccesstoken'); -import * as resthandlers from 'typed-rest-client/Handlers'; import * as rm from 'typed-rest-client/RestClient'; import vsom = require('./VsoClient'); import lim = require("./interfaces/LocationsInterfaces"); @@ -40,19 +43,19 @@ import crypto = require('crypto'); */ export function getBasicHandler(username: string, password: string): VsoBaseInterfaces.IRequestHandler { - return new resthandlers.BasicCredentialHandler(username, password); + return new basicm.BasicCredentialHandler(username, password); } export function getNtlmHandler(username: string, password: string, workstation?: string, domain?: string): VsoBaseInterfaces.IRequestHandler { - return new resthandlers.NtlmCredentialHandler(username, password, workstation, domain); + return new ntlmm.NtlmCredentialHandler(username, password, workstation, domain); } export function getBearerHandler(token: string): VsoBaseInterfaces.IRequestHandler { - return new resthandlers.BearerCredentialHandler(token); + return new bearm.BearerCredentialHandler(token); } export function getPersonalAccessTokenHandler(token: string): VsoBaseInterfaces.IRequestHandler { - return new resthandlers.PersonalAccessTokenCredentialHandler(token); + return new patm.PersonalAccessTokenCredentialHandler(token); } export function getHandlerFromToken(token: string): VsoBaseInterfaces.IRequestHandler { diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts new file mode 100644 index 00000000..c615d1ba --- /dev/null +++ b/api/handlers/basiccreds.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import ifm = require('../interfaces/common/VsoBaseInterfaces'); + +export class BasicCredentialHandler implements ifm.IRequestHandler { + username: string; + password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } + + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options:any): void { + options.headers['Authorization'] = 'Basic ' + new Buffer(this.username + ':' + this.password).toString('base64'); + options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; + } + + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } + + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; + } +} \ No newline at end of file diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts new file mode 100644 index 00000000..cf1388fb --- /dev/null +++ b/api/handlers/bearertoken.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import ifm = require('../interfaces/common/VsoBaseInterfaces'); + +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; + options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; + } + + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } + + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; + } +} \ No newline at end of file diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts new file mode 100644 index 00000000..24563ca3 --- /dev/null +++ b/api/handlers/ntlm.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import ifm = require('../interfaces/common/VsoBaseInterfaces'); +import http = require("http"); +import https = require("https"); + +var _ = require("underscore"); +var ntlm = require("../opensource/node-http-ntlm/ntlm"); + +interface INtlmOptions { + username?: string, + password?: string, + domain: string, + workstation: string +} + +export class NtlmCredentialHandler implements ifm.IRequestHandler { + private _ntlmOptions: INtlmOptions; + + constructor(username: string, password: string, workstation?: string, domain?: string) { + this._ntlmOptions = {}; + + this._ntlmOptions.username = username; + this._ntlmOptions.password = password; + + if (domain !== undefined) { + this._ntlmOptions.domain = domain; + } + if (workstation !== undefined) { + this._ntlmOptions.workstation = workstation; + } + } + + public prepareRequest(options: http.RequestOptions): void { + // No headers or options need to be set. We keep the credentials on the handler itself. + // If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time + if (options.agent) { + delete options.agent; + } + } + + public canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + if (response && response.message && response.message.statusCode === 401) { + // Ensure that we're talking NTLM here + // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM + const wwwAuthenticate = response.message.headers['www-authenticate']; + + if (wwwAuthenticate) { + const mechanisms = wwwAuthenticate.split(', '); + const index = mechanisms.indexOf("NTLM"); + if (index >= 0) { + return true; + } + } + } + + return false; + } + + public handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return new Promise((resolve, reject) => { + var callbackForResult = function (err: any, res: ifm.IHttpClientResponse) { + if(err) { + reject(err); + } + // We have to readbody on the response before continuing otherwise there is a hang. + res.readBody().then(() => { + resolve(res); + }); + }; + + this.handleAuthenticationPrivate(httpClient, requestInfo, objs, callbackForResult); + }); + } + + private handleAuthenticationPrivate(httpClient: any, requestInfo: ifm.IRequestInfo, objs, finalCallback): void { + // Set up the headers for NTLM authentication + requestInfo.options = _.extend(requestInfo.options, { + username: this._ntlmOptions.username, + password: this._ntlmOptions.password, + domain: this._ntlmOptions.domain, + workstation: this._ntlmOptions.workstation + }); + + if (httpClient.isSsl === true) { + requestInfo.options.agent = new https.Agent({ keepAlive: true }); + } else { + requestInfo.options.agent = new http.Agent({ keepAlive: true }); + } + + let self = this; + + // The following pattern of sending the type1 message following immediately (in a setImmediate) is + // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) + // the NTLM exchange will always fail with a 401. + this.sendType1Message(httpClient, requestInfo, objs, function (err, res) { + if (err) { + return finalCallback(err, null, null); + } + + /// We have to readbody on the response before continuing otherwise there is a hang. + res.readBody().then(() => { + // It is critical that we have setImmediate here due to how connection requests are queued. + // If setImmediate is removed then the NTLM handshake will not work. + // setImmediate allows us to queue a second request on the same connection. If this second + // request is not queued on the connection when the first request finishes then node closes + // the connection. NTLM requires both requests to be on the same connection so we need this. + setImmediate(function () { + self.sendType3Message(httpClient, requestInfo, objs, res, finalCallback); + }); + }); + }); + } + + // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js + private sendType1Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, finalCallback): void { + const type1msg = ntlm.createType1Message(this._ntlmOptions); + + const type1options: http.RequestOptions = { + headers: { + 'Connection': 'keep-alive', + 'Authorization': type1msg + }, + timeout: requestInfo.options.timeout || 0, + agent: requestInfo.httpModule, + }; + + const type1info = {}; + type1info.httpModule = requestInfo.httpModule; + type1info.parsedUrl = requestInfo.parsedUrl; + type1info.options = _.extend(type1options, _.omit(requestInfo.options, 'headers')); + + return httpClient.requestRawWithCallback(type1info, objs, finalCallback); + } + + // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js + private sendType3Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, res, callback): void { + if (!res.message.headers && !res.message.headers['www-authenticate']) { + throw new Error('www-authenticate not found on response of second request'); + } + + const type2msg = ntlm.parseType2Message(res.message.headers['www-authenticate']); + const type3msg = ntlm.createType3Message(type2msg, this._ntlmOptions); + + const type3options: http.RequestOptions = { + headers: { + 'Authorization': type3msg, + 'Connection': 'Close' + }, + agent: requestInfo.httpModule, + }; + + const type3info = {}; + type3info.httpModule = requestInfo.httpModule; + type3info.parsedUrl = requestInfo.parsedUrl; + type3options.headers = _.extend(type3options.headers, requestInfo.options.headers); + type3info.options = _.extend(type3options, _.omit(requestInfo.options, 'headers')); + + return httpClient.requestRawWithCallback(type3info, objs, callback); + } +} \ No newline at end of file diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts new file mode 100644 index 00000000..286347a9 --- /dev/null +++ b/api/handlers/personalaccesstoken.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import ifm = require('../interfaces/common/VsoBaseInterfaces'); + +export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { + token: string; + + constructor(token: string) { + this.token = token; + } + + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options:any): void { + options.headers['Authorization'] = 'Basic ' + new Buffer('PAT:' + this.token).toString('base64'); + options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; + } + + // This handler cannot handle 401 + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { + return false; + } + + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; + } +} \ No newline at end of file From 9c0e1b698cf8bd41c55b0ade8c79b3091ca88a28 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Mon, 10 Sep 2018 15:04:55 -0400 Subject: [PATCH 8/9] Updated handlers to use typed-rest-client logic without hurting backwards compat --- api/handlers/basiccreds.ts | 25 +---- api/handlers/bearertoken.ts | 23 +--- api/handlers/ntlm.ts | 157 +--------------------------- api/handlers/personalaccesstoken.ts | 23 +--- 4 files changed, 12 insertions(+), 216 deletions(-) diff --git a/api/handlers/basiccreds.ts b/api/handlers/basiccreds.ts index c615d1ba..71002fac 100644 --- a/api/handlers/basiccreds.ts +++ b/api/handlers/basiccreds.ts @@ -2,29 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import ifm = require('../interfaces/common/VsoBaseInterfaces'); +import * as resthandlers from 'typed-rest-client/Handlers'; -export class BasicCredentialHandler implements ifm.IRequestHandler { - username: string; - password: string; - +export class BasicCredentialHandler extends resthandlers.BasicCredentialHandler implements ifm.IRequestHandler { constructor(username: string, password: string) { - this.username = username; - this.password = password; - } - - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options:any): void { - options.headers['Authorization'] = 'Basic ' + new Buffer(this.username + ':' + this.password).toString('base64'); - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; + super(username, password); } } \ No newline at end of file diff --git a/api/handlers/bearertoken.ts b/api/handlers/bearertoken.ts index cf1388fb..e32941ce 100644 --- a/api/handlers/bearertoken.ts +++ b/api/handlers/bearertoken.ts @@ -2,27 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import ifm = require('../interfaces/common/VsoBaseInterfaces'); +import * as resthandlers from 'typed-rest-client/Handlers'; -export class BearerCredentialHandler implements ifm.IRequestHandler { - token: string; - +export class BearerCredentialHandler extends resthandlers.BearerCredentialHandler implements ifm.IRequestHandler { 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; - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; + super(token); } } \ No newline at end of file diff --git a/api/handlers/ntlm.ts b/api/handlers/ntlm.ts index 24563ca3..c64fba2f 100644 --- a/api/handlers/ntlm.ts +++ b/api/handlers/ntlm.ts @@ -2,161 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import ifm = require('../interfaces/common/VsoBaseInterfaces'); -import http = require("http"); -import https = require("https"); - -var _ = require("underscore"); -var ntlm = require("../opensource/node-http-ntlm/ntlm"); - -interface INtlmOptions { - username?: string, - password?: string, - domain: string, - workstation: string -} - -export class NtlmCredentialHandler implements ifm.IRequestHandler { - private _ntlmOptions: INtlmOptions; +import * as resthandlers from 'typed-rest-client/Handlers'; +export class NtlmCredentialHandler extends resthandlers.NtlmCredentialHandler implements ifm.IRequestHandler { constructor(username: string, password: string, workstation?: string, domain?: string) { - this._ntlmOptions = {}; - - this._ntlmOptions.username = username; - this._ntlmOptions.password = password; - - if (domain !== undefined) { - this._ntlmOptions.domain = domain; - } - if (workstation !== undefined) { - this._ntlmOptions.workstation = workstation; - } - } - - public prepareRequest(options: http.RequestOptions): void { - // No headers or options need to be set. We keep the credentials on the handler itself. - // If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time - if (options.agent) { - delete options.agent; - } - } - - public canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - if (response && response.message && response.message.statusCode === 401) { - // Ensure that we're talking NTLM here - // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM - const wwwAuthenticate = response.message.headers['www-authenticate']; - - if (wwwAuthenticate) { - const mechanisms = wwwAuthenticate.split(', '); - const index = mechanisms.indexOf("NTLM"); - if (index >= 0) { - return true; - } - } - } - - return false; - } - - public handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return new Promise((resolve, reject) => { - var callbackForResult = function (err: any, res: ifm.IHttpClientResponse) { - if(err) { - reject(err); - } - // We have to readbody on the response before continuing otherwise there is a hang. - res.readBody().then(() => { - resolve(res); - }); - }; - - this.handleAuthenticationPrivate(httpClient, requestInfo, objs, callbackForResult); - }); - } - - private handleAuthenticationPrivate(httpClient: any, requestInfo: ifm.IRequestInfo, objs, finalCallback): void { - // Set up the headers for NTLM authentication - requestInfo.options = _.extend(requestInfo.options, { - username: this._ntlmOptions.username, - password: this._ntlmOptions.password, - domain: this._ntlmOptions.domain, - workstation: this._ntlmOptions.workstation - }); - - if (httpClient.isSsl === true) { - requestInfo.options.agent = new https.Agent({ keepAlive: true }); - } else { - requestInfo.options.agent = new http.Agent({ keepAlive: true }); - } - - let self = this; - - // The following pattern of sending the type1 message following immediately (in a setImmediate) is - // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) - // the NTLM exchange will always fail with a 401. - this.sendType1Message(httpClient, requestInfo, objs, function (err, res) { - if (err) { - return finalCallback(err, null, null); - } - - /// We have to readbody on the response before continuing otherwise there is a hang. - res.readBody().then(() => { - // It is critical that we have setImmediate here due to how connection requests are queued. - // If setImmediate is removed then the NTLM handshake will not work. - // setImmediate allows us to queue a second request on the same connection. If this second - // request is not queued on the connection when the first request finishes then node closes - // the connection. NTLM requires both requests to be on the same connection so we need this. - setImmediate(function () { - self.sendType3Message(httpClient, requestInfo, objs, res, finalCallback); - }); - }); - }); - } - - // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType1Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, finalCallback): void { - const type1msg = ntlm.createType1Message(this._ntlmOptions); - - const type1options: http.RequestOptions = { - headers: { - 'Connection': 'keep-alive', - 'Authorization': type1msg - }, - timeout: requestInfo.options.timeout || 0, - agent: requestInfo.httpModule, - }; - - const type1info = {}; - type1info.httpModule = requestInfo.httpModule; - type1info.parsedUrl = requestInfo.parsedUrl; - type1info.options = _.extend(type1options, _.omit(requestInfo.options, 'headers')); - - return httpClient.requestRawWithCallback(type1info, objs, finalCallback); - } - - // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js - private sendType3Message(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs: any, res, callback): void { - if (!res.message.headers && !res.message.headers['www-authenticate']) { - throw new Error('www-authenticate not found on response of second request'); - } - - const type2msg = ntlm.parseType2Message(res.message.headers['www-authenticate']); - const type3msg = ntlm.createType3Message(type2msg, this._ntlmOptions); - - const type3options: http.RequestOptions = { - headers: { - 'Authorization': type3msg, - 'Connection': 'Close' - }, - agent: requestInfo.httpModule, - }; - - const type3info = {}; - type3info.httpModule = requestInfo.httpModule; - type3info.parsedUrl = requestInfo.parsedUrl; - type3options.headers = _.extend(type3options.headers, requestInfo.options.headers); - type3info.options = _.extend(type3options, _.omit(requestInfo.options, 'headers')); - - return httpClient.requestRawWithCallback(type3info, objs, callback); + super(username, password, workstation, domain); } } \ No newline at end of file diff --git a/api/handlers/personalaccesstoken.ts b/api/handlers/personalaccesstoken.ts index 286347a9..ee16a599 100644 --- a/api/handlers/personalaccesstoken.ts +++ b/api/handlers/personalaccesstoken.ts @@ -2,27 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import ifm = require('../interfaces/common/VsoBaseInterfaces'); +import * as resthandlers from 'typed-rest-client/Handlers'; -export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { - token: string; - +export class PersonalAccessTokenCredentialHandler extends resthandlers.PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler { constructor(token: string) { - this.token = token; - } - - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options:any): void { - options.headers['Authorization'] = 'Basic ' + new Buffer('PAT:' + this.token).toString('base64'); - options.headers['X-TFS-FedAuthRedirect'] = 'Suppress'; - } - - // This handler cannot handle 401 - canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { - return false; - } - - handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { - return null; + super(token); } } \ No newline at end of file From 619b8f9608c50d239657142dd20020795e061d25 Mon Sep 17 00:00:00 2001 From: damccorm Date: Mon, 10 Sep 2018 15:30:58 -0400 Subject: [PATCH 9/9] Making getValueString private instead of protected --- api/VsoClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/VsoClient.ts b/api/VsoClient.ts index 46b564f4..142d62f0 100644 --- a/api/VsoClient.ts +++ b/api/VsoClient.ts @@ -207,7 +207,7 @@ export class VsoClient { return value; } - protected getValueString(queryValue, value) { + private getValueString(queryValue, value) { let valueString = null; if (typeof(value) === 'object') { valueString = this.getSerializedObject(queryValue, value); @@ -324,4 +324,4 @@ export class VsoClient { return result; } -} \ No newline at end of file +}