From 75a37890dde0167d662c7bb707845178687decf5 Mon Sep 17 00:00:00 2001 From: Stephen Franceschelli Date: Wed, 17 Jan 2018 17:30:25 -0500 Subject: [PATCH] Add NTLM authorization handler. (#56) * Add NTLM authorization handler. * Code review feedback. --- lib/HttpClient.ts | 199 ++++++++++++++++---------- lib/Interfaces.ts | 42 ++++-- lib/Util.ts | 3 + lib/handlers/basiccreds.ts | 5 +- lib/handlers/bearertoken.ts | 5 +- lib/handlers/ntlm.ts | 165 ++++++++++++--------- lib/handlers/personalaccesstoken.ts | 5 +- lib/opensource/node-http-ntlm/ntlm.js | 1 - package-lock.json | 6 +- package.json | 2 +- samples/handlers.ts | 22 ++- samples/package-lock.json | 18 +++ test/httptests.ts | 8 +- test/package-lock.json | 18 +++ test/resttests.ts | 5 + 15 files changed, 336 insertions(+), 168 deletions(-) diff --git a/lib/HttpClient.ts b/lib/HttpClient.ts index 5f1f80a..525846b 100644 --- a/lib/HttpClient.ts +++ b/lib/HttpClient.ts @@ -36,9 +36,9 @@ export enum HttpCodes { GatewayTimeout = 504, } -const HttpRedirectCodes: number[] = [ HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect ]; +const HttpRedirectCodes: number[] = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect]; -export class HttpClientResponse { +export class HttpClientResponse implements ifm.IHttpClientResponse { constructor(message: http.IncomingMessage) { this.message = message; } @@ -71,11 +71,11 @@ export function isHttps(requestUrl: string) { } enum EnvironmentVariables { - HTTP_PROXY = "HTTP_PROXY", + HTTP_PROXY = "HTTP_PROXY", HTTPS_PROXY = "HTTPS_PROXY", } -export class HttpClient { +export class HttpClient implements ifm.IHttpClient { userAgent: string; handlers: ifm.IRequestHandler[]; requestOptions: ifm.IRequestOptions; @@ -97,7 +97,7 @@ export class HttpClient { constructor(userAgent: string, handlers?: ifm.IRequestHandler[], requestOptions?: ifm.IRequestOptions) { this.userAgent = userAgent; - this.handlers = handlers; + this.handlers = handlers || []; this.requestOptions = requestOptions; if (requestOptions) { if (requestOptions.ignoreSslError != null) { @@ -142,35 +142,35 @@ export class HttpClient { } } - public options(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { + public options(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); } - public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { + public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { return this.request('GET', requestUrl, null, additionalHeaders || {}); } - public del(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + public sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: ifm.IHeaders): Promise { return this.request(verb, requestUrl, stream, additionalHeaders); } @@ -179,13 +179,34 @@ export class HttpClient { * 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 { + 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"); + throw new Error("Client has already been disposed."); } let info: RequestInfo = this._prepareRequest(verb, requestUrl, headers); - let response: HttpClientResponse = await this._requestRaw(info, data); + let response: HttpClientResponse = 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 @@ -204,7 +225,7 @@ export class HttpClient { // let's make the request with the new redirectUrl info = this._prepareRequest(verb, redirectUrl, headers); - response = await this._requestRaw(info, data); + response = await this.requestRaw(info, data); redirectsRemaining--; } @@ -226,63 +247,93 @@ export class HttpClient { this._disposed = true; } - private _requestRaw(info: RequestInfo, data: string | NodeJS.ReadableStream): Promise { - return new Promise((resolve, reject) => { - let socket; - - let isDataString = typeof (data) === 'string'; - - 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); - resolve(res); - }); - - req.on('socket', (sock) => { - socket = sock; - }); - - // If we ever get disconnected, we want the socket to timeout eventually - req.setTimeout(this._socketTimeout || 3 * 60000, () => { - if (socket) { - socket.end(); + /** + * 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); } - reject(new Error('Request timeout: ' + info.options.path)); - }); - req.on('error', function (err) { - // err has statusCode property - // res should have headers - reject(err); - }); + resolve(res); + }; - 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(); - } + this.requestRawWithCallback(info, data, callbackForResult); }); } - private _prepareRequest(method: string, requestUrl: string, headers: any): RequestInfo { - let info: RequestInfo = {}; + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + public requestRawWithCallback(info: ifm.IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: ifm.IHttpClientResponse) => void): void { + let socket; + + let isDataString = typeof (data) === 'string'; + if (typeof (data) === 'string') { + info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8'); + } + + let callbackCalled: boolean = false; + let handleResult = (err: any, res: HttpClientResponse) => { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + }; + + let req: http.ClientRequest = info.httpModule.request(info.options, (msg: http.IncomingMessage) => { + let res: HttpClientResponse = new HttpClientResponse(msg); + handleResult(null, res); + }); + + req.on('socket', (sock) => { + socket = sock; + }); + + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error('Request timeout: ' + info.options.path), null); + }); + + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err, null); + }); + + if (data && typeof (data) === 'string') { + req.write(data, 'utf8'); + } + + if (data && typeof (data) !== 'string') { + data.on('close', function () { + req.end(); + }); + + data.pipe(req); + } + else { + req.end(); + } + } + + private _prepareRequest(method: string, requestUrl: string, headers: any): ifm.IRequestInfo { + const info: ifm.IRequestInfo = {}; info.parsedUrl = url.parse(requestUrl); - let usingSsl = info.parsedUrl.protocol === 'https:'; + const usingSsl: boolean = info.parsedUrl.protocol === 'https:'; info.httpModule = usingSsl ? https : http; - var defaultPort: number = usingSsl ? 443 : 80; + 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; @@ -320,15 +371,15 @@ export class HttpClient { return agent; } - var parsedUrl = url.parse(requestUrl); - let usingSsl = parsedUrl.protocol === 'https:'; + let parsedUrl = url.parse(requestUrl); + const usingSsl = parsedUrl.protocol === 'https:'; let maxSockets = 100; if (!!this.requestOptions) { maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets } if (useProxy) { - var agentOptions: tunnel.TunnelOptions = { + const agentOptions: tunnel.TunnelOptions = { maxSockets: maxSockets, keepAlive: this._keepAlive, proxy: { @@ -338,8 +389,8 @@ export class HttpClient { }, }; - var tunnelAgent: Function; - var overHttps = proxy.proxyUrl.protocol === 'https:'; + let tunnelAgent: Function; + const overHttps = proxy.proxyUrl.protocol === 'https:'; if (usingSsl) { tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; } else { @@ -352,13 +403,13 @@ export class HttpClient { // if reusing agent across request and tunneling agent isn't assigned create a new agent if (this._keepAlive && !agent) { - var options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; + 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) { + if (!agent) { agent = usingSsl ? https.globalAgent : http.globalAgent; } @@ -377,7 +428,7 @@ export class HttpClient { } private _getProxy(requestUrl) { - var parsedUrl = url.parse(requestUrl); + const parsedUrl = url.parse(requestUrl); let usingSsl = parsedUrl.protocol === 'https:'; let proxyConfig: ifm.IProxyConfiguration = this._httpProxy; diff --git a/lib/Interfaces.ts b/lib/Interfaces.ts index 1de4871..c9584e2 100644 --- a/lib/Interfaces.ts +++ b/lib/Interfaces.ts @@ -1,3 +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 http = require("http"); +import url = require("url"); + export interface IHeaders { [key: string]: any }; export interface IBasicCredentials { @@ -5,18 +11,38 @@ export interface IBasicCredentials { password: string; } -export interface IRequestHandler { - prepareRequest(options: any): void; - canHandleAuthentication(res: IHttpResponse): boolean; - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void; +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 IHttpResponse { - statusCode?: number; - headers: any; +export interface IRequestHandler { + prepareRequest(options: http.RequestOptions): void; + canHandleAuthentication(response: IHttpClientResponse): boolean; + handleAuthentication(httpClient: IHttpClient, requestInfo: IRequestInfo, objs): Promise; +} + +export interface IHttpClientResponse { + message: http.IncomingMessage; + readBody(): Promise; +} + +export interface IRequestInfo { + options: http.RequestOptions; + parsedUrl: url.Url; + httpModule: any; } export interface IRequestOptions { + headers?: IHeaders; socketTimeout?: number, ignoreSslError?: boolean, proxy?: IProxyConfiguration, @@ -39,4 +65,4 @@ export interface ICertConfiguration { certFile?: string; keyFile?: string; passphrase?: string; -} \ No newline at end of file +} diff --git a/lib/Util.ts b/lib/Util.ts index 1eaea95..ab8ef84 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + import * as url from 'url'; import * as path from 'path'; diff --git a/lib/handlers/basiccreds.ts b/lib/handlers/basiccreds.ts index 1296dd8..b4030ef 100644 --- a/lib/handlers/basiccreds.ts +++ b/lib/handlers/basiccreds.ts @@ -20,10 +20,11 @@ export class BasicCredentialHandler implements ifm.IRequestHandler { } // This handler cannot handle 401 - canHandleAuthentication(res: ifm.IHttpResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; } } diff --git a/lib/handlers/bearertoken.ts b/lib/handlers/bearertoken.ts index 86a03f7..8b7c5d7 100644 --- a/lib/handlers/bearertoken.ts +++ b/lib/handlers/bearertoken.ts @@ -18,10 +18,11 @@ export class BearerCredentialHandler implements ifm.IRequestHandler { } // This handler cannot handle 401 - canHandleAuthentication(res: ifm.IHttpResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; } } diff --git a/lib/handlers/ntlm.ts b/lib/handlers/ntlm.ts index 81b8c03..039ae77 100644 --- a/lib/handlers/ntlm.ts +++ b/lib/handlers/ntlm.ts @@ -2,30 +2,37 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import ifm = require('../Interfaces'); - import http = require("http"); import https = require("https"); + var _ = require("underscore"); var ntlm = require("../opensource/node-http-ntlm/ntlm"); -export class NtlmCredentialHandler implements ifm.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, domain?: string, workstation?: string) { - this.username = username; - this.password = password; if (domain !== undefined) { - this.domain = domain; + this._ntlmOptions.domain = domain; } if (workstation !== undefined) { - this.workstation = workstation; + 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,93 +40,123 @@ export class NtlmCredentialHandler implements ifm.IRequestHandler { } } - canHandleAuthentication(res: ifm.IHttpResponse): boolean { - if (res && res.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.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, protocol, options, objs, finalCallback): void { - // Set up the headers for NTLM authentication - var ntlmOptions = _.extend(options, { - username: this.username, - password: this.password, - domain: this.domain || '', - workstation: this.workstation || '' + 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); }); - var keepaliveAgent; + } + + 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) { - keepaliveAgent = new https.Agent({}); + 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, protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { + this.sendType1Message(httpClient, requestInfo, objs, function (err, res) { if (err) { return finalCallback(err, null, null); } - setImmediate(function () { - self.sendType3Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, res, finalCallback); + + /// 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, 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, callback): void { - if (!res.headers['www-authenticate']) { - return callback(new Error('www-authenticate not found on response of second request')); + 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, callback); + + 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); } } diff --git a/lib/handlers/personalaccesstoken.ts b/lib/handlers/personalaccesstoken.ts index af5d3da..8c8ada3 100644 --- a/lib/handlers/personalaccesstoken.ts +++ b/lib/handlers/personalaccesstoken.ts @@ -18,10 +18,11 @@ export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler } // This handler cannot handle 401 - canHandleAuthentication(res: ifm.IHttpResponse): boolean { + canHandleAuthentication(response: ifm.IHttpClientResponse): boolean { return false; } - handleAuthentication(httpClient, protocol, options, objs, finalCallback): void { + handleAuthentication(httpClient: ifm.IHttpClient, requestInfo: ifm.IRequestInfo, objs): Promise { + return null; } } diff --git a/lib/opensource/node-http-ntlm/ntlm.js b/lib/opensource/node-http-ntlm/ntlm.js index 5a776c3..adf7602 100644 --- a/lib/opensource/node-http-ntlm/ntlm.js +++ b/lib/opensource/node-http-ntlm/ntlm.js @@ -387,4 +387,3 @@ exports.createType3Message = createType3Message; - diff --git a/package-lock.json b/package-lock.json index d3fbf0e..3e21847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "typed-rest-client", - "version": "1.0.1", + "version": "1.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -23,13 +23,13 @@ "@types/mocha": { "version": "2.2.44", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", - "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", + "integrity": "sha1-HUp5jlPzUhL9WtTQQFBiAXHNW14=", "dev": true }, "@types/node": { "version": "6.0.92", "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.92.tgz", - "integrity": "sha512-awEYSSTn7dauwVCYSx2CJaPTu0Z1Ht2oR1b2AD3CYao6ZRb+opb6EL43fzmD7eMFgMHzTBWSUzlWSD+S8xN0Nw==", + "integrity": "sha1-5/chrigncuEroleZaMANnM5CLF0=", "dev": true }, "@types/shelljs": { diff --git a/package.json b/package.json index ded0e34..03d1f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typed-rest-client", - "version": "1.0.2", + "version": "1.0.3", "description": "Node Rest and Http Clients for use with TypeScript", "main": "./RestClient.js", "scripts": { diff --git a/samples/handlers.ts b/samples/handlers.ts index 4bb1f2f..34fd399 100644 --- a/samples/handlers.ts +++ b/samples/handlers.ts @@ -1,15 +1,23 @@ import * as hm from 'typed-rest-client/Handlers' import * as cm from './common'; +import * as httpm from 'typed-rest-client/HttpClient'; export async function run() { cm.banner('Handler Samples'); + + var username = ""; + var password = ""; + var workstation = ""; + var domain = ""; + var url = ""; - let bh: hm.BasicCredentialHandler = new hm.BasicCredentialHandler('johndoe', 'password'); - let ph: hm.PersonalAccessTokenCredentialHandler = - new hm.PersonalAccessTokenCredentialHandler('scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'); - let nh: hm.NtlmCredentialHandler = new hm.NtlmCredentialHandler('johndoe', 'password'); + const basicHandler: hm.BasicCredentialHandler = new hm.BasicCredentialHandler(username, password); + const patHandler: hm.PersonalAccessTokenCredentialHandler = new hm.PersonalAccessTokenCredentialHandler('scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'); + const ntlmHandler: hm.NtlmCredentialHandler = new hm.NtlmCredentialHandler(username, password, workstation, domain); + // These handlers would then be passed to the constructors of the http or rest modules + + // const httpClient: httpm.HttpClient = new httpm.HttpClient('vsts-node-api', [ntlmHandler]); + // const response: httpm.HttpClientResponse = await httpClient.get(url); + // console.log("response code: " + response.message.statusCode); } - - - diff --git a/samples/package-lock.json b/samples/package-lock.json index 6031f51..f63bd29 100644 --- a/samples/package-lock.json +++ b/samples/package-lock.json @@ -11,6 +11,24 @@ "underscore": "1.8.3" }, "dependencies": { + "httpntlm": { + "version": "1.7.5", + "bundled": true, + "requires": { + "httpreq": "0.4.24", + "underscore": "1.7.0" + }, + "dependencies": { + "underscore": { + "version": "1.7.0", + "bundled": true + } + } + }, + "httpreq": { + "version": "0.4.24", + "bundled": true + }, "tunnel": { "version": "0.0.4", "bundled": true diff --git a/test/httptests.ts b/test/httptests.ts index a731b4f..6b035bb 100644 --- a/test/httptests.ts +++ b/test/httptests.ts @@ -80,7 +80,7 @@ describe('Http Tests', function () { let creds: string = Buffer.from(auth.substring('Basic '.length), 'base64').toString(); assert(creds === 'PAT:' + token, "creds should be the token"); assert(obj.url === "http://httpbin.org/get"); - }); + }); it('pipes a get request', () => { return new Promise(async (resolve, reject) => { @@ -107,12 +107,12 @@ describe('Http Tests', function () { let res: httpm.HttpClientResponse = await http.get("https://httpbin.org/redirect-to?url=" + encodeURIComponent("https://httpbin.org/get")) assert(res.message.statusCode == 302, "status code should be 302"); let body: string = await res.readBody(); - }); + }); it('does basic head request', async() => { let res: httpm.HttpClientResponse = await _http.head('http://httpbin.org/get'); assert(res.message.statusCode == 200, "status code should be 200"); - }); + }); it('does basic http delete request', async() => { let res: httpm.HttpClientResponse = await _http.del('http://httpbin.org/delete'); @@ -139,7 +139,7 @@ describe('Http Tests', function () { let obj:any = JSON.parse(body); assert(obj.data === b); assert(obj.url === "http://httpbin.org/patch"); - }); + }); it('does basic http options request', async() => { let res: httpm.HttpClientResponse = await _http.options('http://httpbin.org'); diff --git a/test/package-lock.json b/test/package-lock.json index 83ab8a5..e852c69 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -11,6 +11,24 @@ "underscore": "1.8.3" }, "dependencies": { + "httpntlm": { + "version": "1.7.5", + "bundled": true, + "requires": { + "httpreq": "0.4.24", + "underscore": "1.7.0" + }, + "dependencies": { + "underscore": { + "version": "1.7.0", + "bundled": true + } + } + }, + "httpreq": { + "version": "0.4.24", + "bundled": true + }, "tunnel": { "version": "0.0.4", "bundled": true diff --git a/test/resttests.ts b/test/resttests.ts index 28defe4..c876a84 100644 --- a/test/resttests.ts +++ b/test/resttests.ts @@ -64,6 +64,8 @@ describe('Rest Tests', function () { }); it('replaces a resource', async() => { + this.timeout(3000); + let res: any = { name: 'foo' }; let restRes: restm.IRestResponse = await _rest.replace('https://httpbin.org/put', res); assert(restRes.statusCode == 200, "statusCode should be 200"); @@ -126,6 +128,8 @@ describe('Rest Tests', function () { // should return a null resource, 404 status, and should not throw // it('gets a non-existant resource (404)', async() => { + this.timeout(3000); + try { let restRes: restm.IRestResponse = await _rest.get('https://httpbin.org/status/404'); @@ -248,6 +252,7 @@ describe('Rest Tests', function () { it('maintains the path from the base url where request has query parameters', async() => { // Arrange + this.timeout(3000); let rest = new restm.RestClient('typed-rest-client-tests', 'https://httpbin.org/anything/multiple'); // Act