From 047bbd092fa1685dd39612db4dfc1e99c687fd5c Mon Sep 17 00:00:00 2001 From: Omesh Pandita Date: Mon, 25 Feb 2019 14:26:24 +0530 Subject: [PATCH] Support ntlm based proxy in tunnel agent. --- lib/ntlm.js | 395 ++++++++++++++++++++++++++++++++++++++++++++++++++ lib/tunnel.js | 142 +++++++++++++++--- 2 files changed, 513 insertions(+), 24 deletions(-) create mode 100644 lib/ntlm.js diff --git a/lib/ntlm.js b/lib/ntlm.js new file mode 100644 index 0000000..f4b860e --- /dev/null +++ b/lib/ntlm.js @@ -0,0 +1,395 @@ +// This software (ntlm.js) was copied from a file of the same name at https://github.com/SamDecrock/node-http-ntlm/blob/master/ntlm.js. +// +// As of this writing, it is a part of the node-http-ntlm module produced by SamDecrock. +// +// It is used as a part of the NTLM proxy support + +var crypto = require('crypto'); + +var flags = { + NTLM_NegotiateUnicode : 0x00000001, + NTLM_NegotiateOEM : 0x00000002, + NTLM_RequestTarget : 0x00000004, + NTLM_Unknown9 : 0x00000008, + NTLM_NegotiateSign : 0x00000010, + NTLM_NegotiateSeal : 0x00000020, + NTLM_NegotiateDatagram : 0x00000040, + NTLM_NegotiateLanManagerKey : 0x00000080, + NTLM_Unknown8 : 0x00000100, + NTLM_NegotiateNTLM : 0x00000200, + NTLM_NegotiateNTOnly : 0x00000400, + NTLM_Anonymous : 0x00000800, + NTLM_NegotiateOemDomainSupplied : 0x00001000, + NTLM_NegotiateOemWorkstationSupplied : 0x00002000, + NTLM_Unknown6 : 0x00004000, + NTLM_NegotiateAlwaysSign : 0x00008000, + NTLM_TargetTypeDomain : 0x00010000, + NTLM_TargetTypeServer : 0x00020000, + NTLM_TargetTypeShare : 0x00040000, + NTLM_NegotiateExtendedSecurity : 0x00080000, + NTLM_NegotiateIdentify : 0x00100000, + NTLM_Unknown5 : 0x00200000, + NTLM_RequestNonNTSessionKey : 0x00400000, + NTLM_NegotiateTargetInfo : 0x00800000, + NTLM_Unknown4 : 0x01000000, + NTLM_NegotiateVersion : 0x02000000, + NTLM_Unknown3 : 0x04000000, + NTLM_Unknown2 : 0x08000000, + NTLM_Unknown1 : 0x10000000, + NTLM_Negotiate128 : 0x20000000, + NTLM_NegotiateKeyExchange : 0x40000000, + NTLM_Negotiate56 : 0x80000000 +}; +var typeflags = { + NTLM_TYPE1_FLAGS : flags.NTLM_NegotiateUnicode + + flags.NTLM_NegotiateOEM + + flags.NTLM_RequestTarget + + flags.NTLM_NegotiateNTLM + + flags.NTLM_NegotiateOemDomainSupplied + + flags.NTLM_NegotiateOemWorkstationSupplied + + flags.NTLM_NegotiateAlwaysSign + + flags.NTLM_NegotiateExtendedSecurity + + flags.NTLM_NegotiateVersion + + flags.NTLM_Negotiate128 + + flags.NTLM_Negotiate56, + + NTLM_TYPE2_FLAGS : flags.NTLM_NegotiateUnicode + + flags.NTLM_RequestTarget + + flags.NTLM_NegotiateNTLM + + flags.NTLM_NegotiateAlwaysSign + + flags.NTLM_NegotiateExtendedSecurity + + flags.NTLM_NegotiateTargetInfo + + flags.NTLM_NegotiateVersion + + flags.NTLM_Negotiate128 + + flags.NTLM_Negotiate56 +}; + +function createType1Message(options){ + var domain = escape(options.domain.toUpperCase()); + var workstation = escape(options.workstation.toUpperCase()); + var protocol = 'NTLMSSP\0'; + + var BODY_LENGTH = 40; + + var type1flags = typeflags.NTLM_TYPE1_FLAGS; + if(!domain || domain === '') + type1flags = type1flags - flags.NTLM_NegotiateOemDomainSupplied; + + var pos = 0; + var buf = new Buffer(BODY_LENGTH + domain.length + workstation.length); + + + buf.write(protocol, pos, protocol.length); pos += protocol.length; // protocol + buf.writeUInt32LE(1, pos); pos += 4; // type 1 + buf.writeUInt32LE(type1flags, pos); pos += 4; // TYPE1 flag + + buf.writeUInt16LE(domain.length, pos); pos += 2; // domain length + buf.writeUInt16LE(domain.length, pos); pos += 2; // domain max length + buf.writeUInt32LE(BODY_LENGTH + workstation.length, pos); pos += 4; // domain buffer offset + + buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation length + buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation max length + buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; // workstation buffer offset + + buf.writeUInt8(5, pos); pos += 1; //ProductMajorVersion + buf.writeUInt8(1, pos); pos += 1; //ProductMinorVersion + buf.writeUInt16LE(2600, pos); pos += 2; //ProductBuild + + buf.writeUInt8(0 , pos); pos += 1; //VersionReserved1 + buf.writeUInt8(0 , pos); pos += 1; //VersionReserved2 + buf.writeUInt8(0 , pos); pos += 1; //VersionReserved3 + buf.writeUInt8(15, pos); pos += 1; //NTLMRevisionCurrent + + buf.write(workstation, pos, workstation.length, 'ascii'); pos += workstation.length; // workstation string + buf.write(domain , pos, domain.length , 'ascii'); pos += domain.length; + + return 'NTLM ' + buf.toString('base64'); +} + +function parseType2Message(rawmsg, callback){ + var match = rawmsg.match(/NTLM (.+)?/); + if(!match || !match[1]) + return callback(new Error("Couldn't find NTLM in the message type2 comming from the server")); + + var buf = new Buffer(match[1], 'base64'); + + var msg = {}; + + msg.signature = buf.slice(0, 8); + msg.type = buf.readInt16LE(8); + + if(msg.type != 2) + return callback(new Error("Server didn't return a type 2 message")); + + msg.targetNameLen = buf.readInt16LE(12); + msg.targetNameMaxLen = buf.readInt16LE(14); + msg.targetNameOffset = buf.readInt32LE(16); + msg.targetName = buf.slice(msg.targetNameOffset, msg.targetNameOffset + msg.targetNameMaxLen); + + msg.negotiateFlags = buf.readInt32LE(20); + msg.serverChallenge = buf.slice(24, 32); + msg.reserved = buf.slice(32, 40); + + if(msg.negotiateFlags & flags.NTLM_NegotiateTargetInfo){ + msg.targetInfoLen = buf.readInt16LE(40); + msg.targetInfoMaxLen = buf.readInt16LE(42); + msg.targetInfoOffset = buf.readInt32LE(44); + msg.targetInfo = buf.slice(msg.targetInfoOffset, msg.targetInfoOffset + msg.targetInfoLen); + } + return msg; +} + +function createType3Message(msg2, options){ + var nonce = msg2.serverChallenge; + var username = options.username; + var password = options.password; + var negotiateFlags = msg2.negotiateFlags; + + var isUnicode = negotiateFlags & flags.NTLM_NegotiateUnicode; + var isNegotiateExtendedSecurity = negotiateFlags & flags.NTLM_NegotiateExtendedSecurity; + + var BODY_LENGTH = 72; + + var domainName = escape(options.domain.toUpperCase()); + var workstation = escape(options.workstation.toUpperCase()); + + var workstationBytes, domainNameBytes, usernameBytes, encryptedRandomSessionKeyBytes; + + var encryptedRandomSessionKey = ""; + if(isUnicode){ + workstationBytes = new Buffer(workstation, 'utf16le'); + domainNameBytes = new Buffer(domainName, 'utf16le'); + usernameBytes = new Buffer(username, 'utf16le'); + encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'utf16le'); + }else{ + workstationBytes = new Buffer(workstation, 'ascii'); + domainNameBytes = new Buffer(domainName, 'ascii'); + usernameBytes = new Buffer(username, 'ascii'); + encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'ascii'); + } + + var lmChallengeResponse = calc_resp(create_LM_hashed_password_v1(password), nonce); + var ntChallengeResponse = calc_resp(create_NT_hashed_password_v1(password), nonce); + + if(isNegotiateExtendedSecurity){ + var pwhash = create_NT_hashed_password_v1(password); + var clientChallenge = ""; + for(var i=0; i < 8; i++){ + clientChallenge += String.fromCharCode( Math.floor(Math.random()*256) ); + } + var clientChallengeBytes = new Buffer(clientChallenge, 'ascii'); + var challenges = ntlm2sr_calc_resp(pwhash, nonce, clientChallengeBytes); + lmChallengeResponse = challenges.lmChallengeResponse; + ntChallengeResponse = challenges.ntChallengeResponse; + } + + var signature = 'NTLMSSP\0'; + + var pos = 0; + var buf = new Buffer(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length + encryptedRandomSessionKeyBytes.length); + + buf.write(signature, pos, signature.length); pos += signature.length; + buf.writeUInt32LE(3, pos); pos += 4; // type 1 + + buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseLen + buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseMaxLen + buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length, pos); pos += 4; // LmChallengeResponseOffset + + buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseLen + buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseMaxLen + buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length, pos); pos += 4; // NtChallengeResponseOffset + + buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameLen + buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameMaxLen + buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; // DomainNameOffset + + buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameLen + buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameMaxLen + buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length, pos); pos += 4; // UserNameOffset + + buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationLen + buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationMaxLen + buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length, pos); pos += 4; // WorkstationOffset + + buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyLen + buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyMaxLen + buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length, pos); pos += 4; // EncryptedRandomSessionKeyOffset + + buf.writeUInt32LE(typeflags.NTLM_TYPE2_FLAGS, pos); pos += 4; // NegotiateFlags + + buf.writeUInt8(5, pos); pos++; // ProductMajorVersion + buf.writeUInt8(1, pos); pos++; // ProductMinorVersion + buf.writeUInt16LE(2600, pos); pos += 2; // ProductBuild + buf.writeUInt8(0, pos); pos++; // VersionReserved1 + buf.writeUInt8(0, pos); pos++; // VersionReserved2 + buf.writeUInt8(0, pos); pos++; // VersionReserved3 + buf.writeUInt8(15, pos); pos++; // NTLMRevisionCurrent + + domainNameBytes.copy(buf, pos); pos += domainNameBytes.length; + usernameBytes.copy(buf, pos); pos += usernameBytes.length; + workstationBytes.copy(buf, pos); pos += workstationBytes.length; + lmChallengeResponse.copy(buf, pos); pos += lmChallengeResponse.length; + ntChallengeResponse.copy(buf, pos); pos += ntChallengeResponse.length; + encryptedRandomSessionKeyBytes.copy(buf, pos); pos += encryptedRandomSessionKeyBytes.length; + + return 'NTLM ' + buf.toString('base64'); +} + +function create_LM_hashed_password_v1(password){ + // fix the password length to 14 bytes + password = password.toUpperCase(); + var passwordBytes = new Buffer(password, 'ascii'); + + var passwordBytesPadded = new Buffer(14); + passwordBytesPadded.fill("\0"); + var sourceEnd = 14; + if(passwordBytes.length < 14) sourceEnd = passwordBytes.length; + passwordBytes.copy(passwordBytesPadded, 0, 0, sourceEnd); + + // split into 2 parts of 7 bytes: + var firstPart = passwordBytesPadded.slice(0,7); + var secondPart = passwordBytesPadded.slice(7); + + function encrypt(buf){ + var key = insertZerosEvery7Bits(buf); + var des = crypto.createCipheriv('DES-ECB', key, ''); + return des.update("KGS!@#$%"); // page 57 in [MS-NLMP]); + } + + var firstPartEncrypted = encrypt(firstPart); + var secondPartEncrypted = encrypt(secondPart); + + return Buffer.concat([firstPartEncrypted, secondPartEncrypted]); +} + +function insertZerosEvery7Bits(buf){ + var binaryArray = bytes2binaryArray(buf); + var newBinaryArray = []; + for(var i=0; i array.length) + break; + + var binString1 = '' + array[i] + '' + array[i+1] + '' + array[i+2] + '' + array[i+3]; + var binString2 = '' + array[i+4] + '' + array[i+5] + '' + array[i+6] + '' + array[i+7]; + var hexchar1 = binary2hex[binString1]; + var hexchar2 = binary2hex[binString2]; + + var buf = new Buffer(hexchar1 + '' + hexchar2, 'hex'); + bufArray.push(buf); + } + + return Buffer.concat(bufArray); +} + +function create_NT_hashed_password_v1(password){ + var buf = new Buffer(password, 'utf16le'); + var md4 = crypto.createHash('md4'); + md4.update(buf); + return new Buffer(md4.digest()); +} + +function calc_resp(password_hash, server_challenge){ + // padding with zeros to make the hash 21 bytes long + var passHashPadded = new Buffer(21); + passHashPadded.fill("\0"); + password_hash.copy(passHashPadded, 0, 0, password_hash.length); + + var resArray = []; + + var des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(0,7)), ''); + resArray.push( des.update(server_challenge.slice(0,8)) ); + + des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(7,14)), ''); + resArray.push( des.update(server_challenge.slice(0,8)) ); + + des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(14,21)), ''); + resArray.push( des.update(server_challenge.slice(0,8)) ); + + return Buffer.concat(resArray); +} + +function ntlm2sr_calc_resp(responseKeyNT, serverChallenge, clientChallenge){ + // padding with zeros to make the hash 16 bytes longer + var lmChallengeResponse = new Buffer(clientChallenge.length + 16); + lmChallengeResponse.fill("\0"); + clientChallenge.copy(lmChallengeResponse, 0, 0, clientChallenge.length); + + var buf = Buffer.concat([serverChallenge, clientChallenge]); + var md5 = crypto.createHash('md5'); + md5.update(buf); + var sess = md5.digest(); + var ntChallengeResponse = calc_resp(responseKeyNT, sess.slice(0,8)); + + return { + lmChallengeResponse: lmChallengeResponse, + ntChallengeResponse: ntChallengeResponse + }; +} + +exports.createType1Message = createType1Message; +exports.parseType2Message = parseType2Message; +exports.createType3Message = createType3Message; + + + diff --git a/lib/tunnel.js b/lib/tunnel.js index 10cc382..15fcdeb 100644 --- a/lib/tunnel.js +++ b/lib/tunnel.js @@ -7,7 +7,9 @@ var https = require('https'); var events = require('events'); var assert = require('assert'); var util = require('util'); +var ntlm = require('./ntlm'); +var os = require("os") exports.httpOverHttp = httpOverHttp; exports.httpsOverHttp = httpsOverHttp; @@ -51,6 +53,7 @@ function TunnelingAgent(options) { self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; self.requests = []; self.sockets = []; + self.freeSockets = []; self.on('free', function onFree(socket, host, port, localAddress) { var options = toOptions(host, port, localAddress); @@ -61,11 +64,26 @@ function TunnelingAgent(options) { // reuse the connection. self.requests.splice(i, 1); pending.request.onSocket(socket); + pending.request.end(); return; } } - socket.destroy(); - self.removeSocket(socket); + + if (!self.keepAlive) { + socket.destroy(); + self.removeSocket(socket); + } + else { + // save the socket for reuse later + self.removeSocket(socket); + socket.removeAllListeners(); + if (!!self.freeSockets[options.host + ':' + options.port]) { + self.freeSockets[options.host + ':' + options.port].push(socket); + } + else { + self.freeSockets[options.host + ':' + options.port] = [socket]; + } + } }); } util.inherits(TunnelingAgent, events.EventEmitter); @@ -80,12 +98,24 @@ TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, local return; } + if (self.keepAlive) { + var socket = !!self.freeSockets[options.host + ':' + options.port] ? self.freeSockets[options.host + ':' + options.port].pop() : undefined; + if(socket){ + this.sockets.push(socket); + executeRequest(socket); + return + } + } + // If we are under maxSockets create a new one. - self.createSocket(options, function(socket) { + self.createSocket(options, executeRequest); + + function executeRequest(socket) { socket.on('free', onFree); socket.on('close', onCloseOrRemove); socket.on('agentRemove', onCloseOrRemove); req.onSocket(socket); + req.end(); function onFree() { self.emit('free', socket, options); @@ -97,18 +127,22 @@ TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, local socket.removeListener('close', onCloseOrRemove); socket.removeListener('agentRemove', onCloseOrRemove); } - }); + } }; TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { var self = this; var placeholder = {}; self.sockets.push(placeholder); - + const keepAliveAgent = new http.Agent({ + keepAlive: true, + maxSockets: 1, + maxFreeSockets: 1 + }) var connectOptions = mergeOptions({}, self.proxyOptions, { method: 'CONNECT', path: options.host + ':' + options.port, - agent: false, + agent: keepAliveAgent, headers: { host: options.host + ':' + options.port } @@ -116,10 +150,17 @@ TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { if (options.localAddress) { connectOptions.localAddress = options.localAddress; } + if (connectOptions.proxyAuth) { connectOptions.headers = connectOptions.headers || {}; - connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + if (typeof (connectOptions.proxyAuth) === 'string') { + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + else { + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth.username + ':' + connectOptions.proxyAuth.password).toString('base64'); + } } debug('making CONNECT request'); @@ -147,29 +188,82 @@ TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { connectReq.removeAllListeners(); socket.removeAllListeners(); - if (res.statusCode !== 200) { - debug('tunneling socket could not be established, statusCode=%d', - res.statusCode); - socket.destroy(); - var error = new Error('tunneling socket could not be established, ' + - 'statusCode=' + res.statusCode); - error.code = 'ECONNRESET'; - options.request.emit('error', error); - self.removeSocket(placeholder); - return; + if(res.statusCode !== 200 && res.headers["proxy-authenticate"] == 'NTLM') { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Connection'] = 'keep-alive', + connectOptions.headers['connection'] = 'keep-alive'; + + if (typeof (connectOptions.proxyAuth) === 'string') { + throw "Expected proxyAuth to be object with username and password fields" + } + + var ntlmOpts = { + domain: '', + workstation: os.hostname(), + username: connectOptions.proxyAuth.username, + password: connectOptions.proxyAuth.password + }; + + connectOptions.headers['proxy-authorization'] = ntlm.createType1Message(ntlmOpts); + connectOptions.headers['keep-alive'] = 'true'; + + connectReq = self.request(connectOptions) + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); } - if (head.length > 0) { - debug('got illegal response body from proxy'); + else if (res.statusCode !== 200 && !!res.headers["proxy-authenticate"] && res.headers["proxy-authenticate"].indexOf('NTLM') !== -1) { + + var type2msg = ntlm.parseType2Message(res.headers['proxy-authenticate'], function (error) { + return null; + }.bind(this)); + if (!type2msg) { + return; + } + + var ntlmOpts = { + domain: '', + workstation: os.hostname(), + username: connectOptions.proxyAuth.username, + password: connectOptions.proxyAuth.password + }; + connectOptions.headers['proxy-authorization'] = ntlm.createType3Message(type2msg, ntlmOpts); + + connectOptions.agent.freeSockets[connectOptions.agent.getName(connectOptions)] = [socket]; + connectReq = self.request(connectOptions) + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + } + else if (res.statusCode != 200) { + debug('tunneling socket could not be established, statusCode=%d, statusMessage=%d', res.statusCode, res.statusMessage); socket.destroy(); - var error = new Error('got illegal response body from proxy'); + var error = new Error('tunneling socket could not be established, statusCode=' + res.statusCode + ' statusMessage=' + res.statusMessage); error.code = 'ECONNRESET'; options.request.emit('error', error); self.removeSocket(placeholder); return; } - debug('tunneling connection has established'); - self.sockets[self.sockets.indexOf(placeholder)] = socket; - return cb(socket); + + if (res.statusCode == 200) + { + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } } function onError(cause) { @@ -198,6 +292,7 @@ TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { // needs to be created to take over in the pool for the one that closed. this.createSocket(pending, function(socket) { pending.request.onSocket(socket); + pending.request.end(); }); } }; @@ -218,7 +313,6 @@ function createSecureSocket(options, cb) { }); } - function toOptions(host, port, localAddress) { if (typeof host === 'string') { // since v0.10 return {