Add NTLM authorization handler. (#56)
* Add NTLM authorization handler. * Code review feedback.
This commit is contained in:
Родитель
9e6a31e88e
Коммит
75a37890dd
|
@ -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<HttpClientResponse> {
|
||||
public options(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('OPTIONS', requestUrl, null, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public get(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('GET', requestUrl, null, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public del(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public del(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('DELETE', requestUrl, null, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public post(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public post(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('POST', requestUrl, data, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public patch(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public patch(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('PATCH', requestUrl, data, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public put(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public put(requestUrl: string, data: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('PUT', requestUrl, data, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public head(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public head(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('HEAD', requestUrl, null, additionalHeaders || {});
|
||||
}
|
||||
|
||||
public sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: ifm.IHeaders): Promise<HttpClientResponse> {
|
||||
public sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
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<HttpClientResponse> {
|
||||
public async request(verb: string, requestUrl: string, data: string | NodeJS.ReadableStream, headers: ifm.IHeaders): Promise<ifm.IHttpClientResponse> {
|
||||
if (this._disposed) {
|
||||
throw new Error("Client has already been disposed");
|
||||
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<HttpClientResponse> {
|
||||
return new Promise<HttpClientResponse>((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<ifm.IHttpClientResponse> {
|
||||
return new Promise<ifm.IHttpClientResponse>((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 = <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 = <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 = <http.RequestOptions>{};
|
||||
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;
|
||||
|
||||
|
|
|
@ -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<IHttpClientResponse>;
|
||||
get(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
del(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
post(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
patch(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
put(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
|
||||
request(verb: string, requestUrl: string, data: string | NodeJS.ReadableStream, headers: IHeaders): Promise<IHttpClientResponse>;
|
||||
requestRaw(info: IRequestInfo, data: string | NodeJS.ReadableStream): Promise<IHttpClientResponse>;
|
||||
requestRawWithCallback(info: IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: IHttpClientResponse) => void): void;
|
||||
}
|
||||
|
||||
export interface IHttpResponse {
|
||||
statusCode?: number;
|
||||
headers: any;
|
||||
export interface IRequestHandler {
|
||||
prepareRequest(options: http.RequestOptions): void;
|
||||
canHandleAuthentication(response: IHttpClientResponse): boolean;
|
||||
handleAuthentication(httpClient: IHttpClient, requestInfo: IRequestInfo, objs): Promise<IHttpClientResponse>;
|
||||
}
|
||||
|
||||
export interface IHttpClientResponse {
|
||||
message: http.IncomingMessage;
|
||||
readBody(): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IRequestInfo {
|
||||
options: http.RequestOptions;
|
||||
parsedUrl: url.Url;
|
||||
httpModule: any;
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
headers?: IHeaders;
|
||||
socketTimeout?: number,
|
||||
ignoreSslError?: boolean,
|
||||
proxy?: IProxyConfiguration,
|
||||
|
@ -39,4 +65,4 @@ export interface ICertConfiguration {
|
|||
certFile?: string;
|
||||
keyFile?: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<ifm.IHttpClientResponse> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ifm.IHttpClientResponse> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = <INtlmOptions>{};
|
||||
|
||||
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<ifm.IHttpClientResponse> {
|
||||
return new Promise<ifm.IHttpClientResponse>((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 = <ifm.IRequestInfo>{};
|
||||
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 = <ifm.IRequestInfo>{};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ifm.IHttpClientResponse> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -387,4 +387,3 @@ exports.createType3Message = createType3Message;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string>(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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<HttpBinData> = await _rest.replace<HttpBinData>('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<HttpBinData> = await _rest.get<HttpBinData>('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
|
||||
|
|
Загрузка…
Ссылка в новой задаче