blob: 69bec4afcbd66d72bbed85b4bee210d8ed7a3c1d [file] [log] [blame]
"use strict";
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
Object.defineProperty(exports, "__esModule", { value: true });
const http = require("http");
const https = require("https");
const _ = require("underscore");
const ntlm = require("../opensource/Node-SMB/lib/ntlm");
class NtlmCredentialHandler {
constructor(username, password, workstation, domain) {
this._ntlmOptions = {};
this._ntlmOptions.username = username;
this._ntlmOptions.password = password;
this._ntlmOptions.domain = domain || '';
this._ntlmOptions.workstation = workstation || '';
}
prepareRequest(options) {
// 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;
}
}
canHandleAuthentication(response) {
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'];
return wwwAuthenticate && (wwwAuthenticate.split(', ').indexOf("NTLM") >= 0);
}
return false;
}
handleAuthentication(httpClient, requestInfo, objs) {
return new Promise((resolve, reject) => {
const callbackForResult = function (err, res) {
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);
});
}
handleAuthenticationPrivate(httpClient, requestInfo, objs, finalCallback) {
// 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
});
requestInfo.options.agent = httpClient.isSsl ?
new https.Agent({ keepAlive: true }) :
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
sendType1Message(httpClient, requestInfo, objs, finalCallback) {
const type1HexBuffer = ntlm.encodeType1(this._ntlmOptions.workstation, this._ntlmOptions.domain);
const type1msg = `NTLM ${type1HexBuffer.toString('base64')}`;
const type1options = {
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
sendType3Message(httpClient, requestInfo, objs, res, callback) {
if (!res.message.headers && !res.message.headers['www-authenticate']) {
throw new Error('www-authenticate not found on response of second request');
}
/**
* Server will respond with challenge/nonce
* assigned to response's "WWW-AUTHENTICATE" header
* and should adhere to RegExp /^NTLM\s+(.+?)(,|\s+|$)/
*/
const serverNonceRegex = /^NTLM\s+(.+?)(,|\s+|$)/;
const serverNonce = Buffer.from((res.message.headers['www-authenticate'].match(serverNonceRegex) || [])[1], 'base64');
let type2msg;
/**
* Wrap decoding the Server's challenge/nonce in
* try-catch block to throw more comprehensive
* Error with clear message to consumer
*/
try {
type2msg = ntlm.decodeType2(serverNonce);
}
catch (error) {
throw new Error(`Decoding Server's Challenge to Obtain Type2Message failed with error: ${error.message}`);
}
const type3msg = ntlm.encodeType3(this._ntlmOptions.username, this._ntlmOptions.workstation, this._ntlmOptions.domain, type2msg, this._ntlmOptions.password).toString('base64');
const type3options = {
headers: {
'Authorization': `NTLM ${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);
}
}
exports.NtlmCredentialHandler = NtlmCredentialHandler;