UNPKG

napi-ldap

Version:
390 lines (346 loc) 9.38 kB
const binding = require("bindings")("napi_ldap"); var LDAPError = require("./LDAPError"); var assert = require("assert"); var util = require("util"); function arg(val, def) { if (val !== undefined) { return val; } return def; } function extendobj(target, other) { var keys = Object.keys(other); for (var index = 0; index < keys.length; ++index) { var key = keys[index]; target[key] = other[key]; } return target; } var escapes = { filter: { regex: new RegExp(/\0|\(|\)|\*|\\/g), replacements: { "\0": "\\00", "(": "\\28", ")": "\\29", "*": "\\2A", "\\": "\\5C" } }, dn: { regex: new RegExp(/\0|\"|\+|\,|;|<|>|=|\\/g), replacements: { "\0": "\\00", " ": "\\ ", '"': '\\"', "#": "\\#", "+": "\\+", ",": "\\,", ";": "\\;", "<": "\\<", ">": "\\>", "=": "\\=", "\\": "\\5C" } } }; function Stats() { this.lateresponses = 0; this.reconnects = 0; this.timeouts = 0; this.requests = 0; this.searches = 0; this.binds = 0; this.errors = 0; this.modifies = 0; this.adds = 0; this.removes = 0; this.renames = 0; this.disconnects = 0; this.results = 0; return this; } function LDAP(opt, fn) { this.queue = {}; this.stats = new Stats(); this.options = extendobj( { base: "dc=com", filter: "(objectClass=*)", scope: 2, attrs: "*", ntimeout: 1000, timeout: 2000, debug: 0, ca: "", validatecert: LDAP.LDAP_OPT_X_TLS_HARD, referrals: 0, connect: function() {}, disconnect: function() {} }, opt ); if (typeof this.options.uri === "string") { this.options.uri = [this.options.uri]; } this.ld = new binding.LDAPCnx( this.dequeue.bind(this), this.onconnect.bind(this), this.ondisconnect.bind(this), this.options.uri.join(" "), this.options.ntimeout, this.options.debug, // Case to int because the c code expects // an int. +this.options.validatecert, this.options.referrals, this.options.ca || "" ); if (typeof fn !== "function") { fn = function() {}; } return this.enqueue(this.ld.bind(undefined, undefined), fn); } LDAP.prototype.onconnect = function() { this.stats.reconnects++; return this.options.connect.call(this); }; LDAP.prototype.ondisconnect = function() { this.stats.disconnects++; this.options.disconnect(); }; LDAP.prototype.starttls = function(fn) { return this.enqueue(this.ld.starttls(), fn); }; LDAP.prototype.installtls = function() { return this.ld.installtls(); }; LDAP.prototype.tlsactive = function() { return this.ld.checktls(); }; LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { this.stats.removes++; if (typeof dn !== "string" || typeof fn !== "function") { throw new LDAPError("Missing argument"); } return this.enqueue(this.ld.delete(dn), fn); }; LDAP.prototype.bind = LDAP.prototype.simplebind = function(opt, fn) { this.stats.binds++; if ( typeof opt === "undefined" || typeof opt.binddn !== "string" || typeof opt.password !== "string" || typeof fn !== "function" ) { throw new LDAPError("Missing argument"); } return this.enqueue(this.ld.bind(opt.binddn, opt.password), fn); }; LDAP.prototype.saslbind = function(opt, fn) { this.stats.binds++; if (arguments.length == 1 && typeof arguments[0] === "function") { fn = opt; opt = undefined; } var args = [ "mechanism", "user", "password", "realm", "proxyuser", "securityproperties" ].map(function(p) { return opt == null ? undefined : opt[p]; }); if ( args.filter(function(a) { return a != null && typeof a !== "string"; }).length || typeof fn !== "function" ) { throw new LDAPError("Invalid argument"); } return this.enqueue(this.ld.saslbind.apply(this.ld, args), fn); }; LDAP.prototype.add = function(dn, attrs, fn) { this.stats.adds++; if (typeof dn !== "string" || typeof attrs !== "object") { throw new LDAPError("Missing argument"); } return this.enqueue(this.ld.add(dn, attrs), fn); }; LDAP.prototype.search = function(opt, fn) { this.stats.searches++; return this.enqueue( this.ld.search( arg(opt.base, this.options.base), arg(opt.filter, this.options.filter), arg(opt.attrs, this.options.attrs), arg(opt.scope, this.options.scope), arg(opt.pagesize, this.options.pagesize), arg(opt.cookie, null) ), unwrap_cookie ); function unwrap_cookie(err, data) { err ? fn(err) : fn(err, data.data, data.cookie); } }; LDAP.prototype.rename = function(dn, newrdn, fn) { this.stats.renames++; if ( typeof dn !== "string" || typeof newrdn !== "string" || typeof fn !== "function" ) { throw new LDAPError("Missing argument"); } return this.enqueue(this.ld.rename(dn, newrdn), fn); }; LDAP.prototype.modify = function(dn, ops, fn) { this.stats.modifies++; if ( typeof dn !== "string" || typeof ops !== "object" || typeof fn !== "function" ) { throw new LDAPError("Missing argument"); } return this.enqueue(this.ld.modify(dn, ops), fn); }; LDAP.prototype.findandbind = function(opt, fn) { if (opt === undefined || opt.password === undefined) { throw new Error("Missing argument"); } this.search( opt, function findandbindFind(err, data) { if (err) return fn(err); if (data === undefined || data.length != 1) { return fn( new LDAPError( "Search returned " + data.length + " results, expected 1" ) ); } if (this.auth_connection === undefined) { this.auth_connection = new LDAP( this.options, function newAuthConnection(err) { if (err) return fn(err); return this.authbind( data[0].dn, opt.password, function authbindResult(err) { fn(err, data[0]); } ); }.bind(this) ); } else { this.authbind(data[0].dn, opt.password, function authbindResult(err) { fn(err, data[0]); }); } return undefined; }.bind(this) ); }; LDAP.prototype.authbind = function(dn, password, fn) { this.auth_connection.bind({ binddn: dn, password: password }, fn.bind(this)); }; LDAP.prototype.close = function() { if (this.auth_connection !== undefined) { this.auth_connection.close(); } this.ld.close(); this.ld = undefined; }; LDAP.prototype.dequeue = function(err, msgid, data) { try { this.stats.results++; if (this.queue[msgid]) { clearTimeout(this.queue[msgid].timer); this.queue[msgid](err, data); delete this.queue[msgid]; } else { this.stats.lateresponses++; } } catch (e) { console.error(e); } }; LDAP.prototype.enqueue = function(msgid, fn) { if (msgid == -1 || this.ld === undefined) { if (this.ld.errorstring() === "Can't contact LDAP server") { // this means we have had a disconnect event, but since there // are still requests outstanding from libldap's perspective, // the connection isn't "closed" and the disconnect event has // not yet fired. To get libldap to actually call the disconnect // handler, we need to dump all outstanding requests, and hope // we're not missing one for some reason. Only once we've // abandoned everything does the handle properly close. Object.keys(this.queue).forEach( function fireTimeout(msgid) { this.queue[msgid](new LDAPError("Timeout")); delete this.queue[msgid]; this.ld.abandon(+msgid); }.bind(this) ); } process.nextTick( function emitError() { fn(new LDAPError(this.ld.errorstring())); }.bind(this) ); this.stats.errors++; return this; } fn.timer = setTimeout( function searchTimeout() { /* in this instance the connection has already been closed */ if (!this.ld) return; this.ld.abandon(msgid); delete this.queue[msgid]; fn(new LDAPError("Timeout")); this.stats.timeouts++; }.bind(this), this.options.timeout ); this.queue[msgid] = fn; this.stats.requests++; return this; }; function stringescape(escapes_obj, str) { return str.replace(escapes_obj.regex, function(match) { return escapes_obj.replacements[match]; }); } LDAP.escapefn = function(type, template) { var escapes_obj = escapes[type]; return function() { var args = [template], i; for (i = 0; i < arguments.length; i++) { // optimizer-friendly args.push(stringescape(escapes_obj, arguments[i])); } return util.format.apply(this, args); }; }; LDAP.stringEscapeFilter = LDAP.escapefn("filter", "%s"); function setConst(target, name, val) { target.prototype[name] = target[name] = val; } setConst(LDAP, "BASE", 0); setConst(LDAP, "ONELEVEL", 1); setConst(LDAP, "SUBTREE", 2); setConst(LDAP, "SUBORDINATE", 3); setConst(LDAP, "DEFAULT", 4); setConst(LDAP, "LDAP_OPT_X_TLS_NEVER", 0); setConst(LDAP, "LDAP_OPT_X_TLS_HARD", 1); setConst(LDAP, "LDAP_OPT_X_TLS_DEMAND", 2); setConst(LDAP, "LDAP_OPT_X_TLS_ALLOW", 3); setConst(LDAP, "LDAP_OPT_X_TLS_TRY", 4); module.exports = LDAP;