napi-ldap
Version:
OpenLDAP bindings for node
390 lines (346 loc) • 9.38 kB
JavaScript
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;