From 18ddbd19cb54a786e931c7310ff527fa99f07902 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 21 Oct 2015 18:07:30 -0600 Subject: [PATCH 01/49] Better documentation for findandbind --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 666da42..d61f995 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,13 @@ ldap.open(function(err) { // connection is ready. }); -ldap.simplebind() +ldap.bind() ----------------- Calling open automatically does an anonymous bind to check to make sure the connection is actually open. If you call simplebind(), you will upgrade the existing anonymous bind. - ldap.simplebind(bind_options, function(err)); + ldap.bind(bind_options, function(err)); Options are binddn and password: @@ -96,6 +96,8 @@ bind_options = { password: '' } ``` +Aliased to `ldap.simplebind()` for backward compatibility. + ldap.search() ------------- @@ -219,10 +221,44 @@ options as the primary connection to attempt to authenticate to LDAP as the user found in the first step. The idea here is to bind your main LDAP instance with an "admin-like" -account that has the permissions to search. Your secondary connection -can then just attempt to authenticate to it's heart's content. +account that has the permissions to search. Your (hidden) secondary +connection will be used only for authenticating users. + +In contrast, the `bind()` method will, if successful, change the +authentication on the primary connection. + +```js +ldap.bind({ + binddn: 'cn=admin,dc=com', + password: 'supersecret' +}, function(err, data) { + if (err) { + ... + } + // now we're authenticated as admin on the main connection + // and thus have the correct permissions for search + + ldap.findandbind({ + filter: '(&(username=johndoe)(status=enabled))', + attrs: 'username homeDirectory' + }, function(err, data) { + if (err) { + ... + } + // our main connection is still cn=admin + // but there's a hidden connection bound + // as "johndoe" + console.log(data[0].homeDirectory[0]); + } +} + +``` + +If you ensure that the "admin" user (or whatever you bind as for +the main connection) can not READ the password field, then +passwords will never leave the LDAP server -- all authentication +is done my the LDAP server itself. -`bind()` itself will change the authentication on the primary connection. ldap.add() ---------- From 0d861535fbce7671e27d0bb97277270ce7a41c58 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 21 Oct 2015 18:08:57 -0600 Subject: [PATCH 02/49] Typo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d61f995..c163641 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ ldap.open(function(err) { } // connection is ready. }); +``` ldap.bind() ----------------- From 65bd0fdd4254d538ac3a68c9b117ff6f4a90bdde Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 21 Oct 2015 18:15:06 -0600 Subject: [PATCH 03/49] Added rebind example --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c163641..75a55bc 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,22 @@ var ldap = new LDAP({ ``` -The reconnect handler is a good place to put a bind() call if you need one. This will rebind on every -reconnect (which is probably what you want). +The reconnect handler is called on initial connect as well, so this function is a really good place +to do a bind() or any other things you want to set up for every connection. + +```js +var ldap = new LDAP({ + uri: 'ldap://server', + reconnect: function() { + ldap.bind({ + binddn: 'cn=admin,dc=com', + password: 'supersecret' + }, function(err) { + ... + }); + } +} +``` ldap.open() ----------- From 9630397d4201ffef1b0edc2c95a22a3236c52e62 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Fri, 23 Oct 2015 15:02:30 -0600 Subject: [PATCH 04/49] Added Close() and TLS. Needs a cleanup. --- .gitignore | 1 + LDAPCnx.cc | 112 +++++++++++++++++++++++++++-------------- LDAPCnx.h | 15 ++++-- index.js | 66 ++++++++++++++---------- test/certs/._.DS_Store | Bin 0 -> 4096 bytes test/certs/device.crt | 22 ++++++++ test/certs/device.csr | 18 +++++++ test/certs/device.key | 27 ++++++++++ test/certs/rootCA.key | 30 +++++++++++ test/certs/rootCA.pem | 27 ++++++++++ test/certs/rootCA.srl | 1 + test/index.js | 17 ++++++- test/run_server.sh | 2 +- test/slapd.conf | 4 ++ test/tls.js | 50 ++++++++++++++++++ 15 files changed, 320 insertions(+), 72 deletions(-) create mode 100644 test/certs/._.DS_Store create mode 100644 test/certs/device.crt create mode 100644 test/certs/device.csr create mode 100644 test/certs/device.key create mode 100644 test/certs/rootCA.key create mode 100644 test/certs/rootCA.pem create mode 100644 test/certs/rootCA.srl create mode 100644 test/tls.js diff --git a/.gitignore b/.gitignore index 6124299..d7b4ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ openldap-data test/slapd.args test/slapd.pid test/reconnect.js +.DS_Store diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 9ec7f51..83b155d 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -11,8 +11,8 @@ LDAPCnx::LDAPCnx() { LDAPCnx::~LDAPCnx() { free(this->ldap_callback); - delete this->callback; - delete this->reconnect_callback; + delete this->result_callback; + delete this->ready_callback; } void LDAPCnx::Init(Local exports) { @@ -31,9 +31,13 @@ void LDAPCnx::Init(Local exports) { Nan::SetPrototypeMethod(tpl, "modify", Modify); Nan::SetPrototypeMethod(tpl, "rename", Rename); Nan::SetPrototypeMethod(tpl, "initialize", Initialize); + Nan::SetPrototypeMethod(tpl, "close", Close); Nan::SetPrototypeMethod(tpl, "errorstring", GetErr); Nan::SetPrototypeMethod(tpl, "errorno", GetErrNo); Nan::SetPrototypeMethod(tpl, "fd", GetFD); + Nan::SetPrototypeMethod(tpl, "starttls", StartTLS); + Nan::SetPrototypeMethod(tpl, "installtls", InstallTLS); + Nan::SetPrototypeMethod(tpl, "checktls", CheckTLS); constructor.Reset(tpl->GetFunction()); exports->Set(Nan::New("LDAPCnx").ToLocalChecked(), tpl->GetFunction()); @@ -45,10 +49,8 @@ void LDAPCnx::New(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = new LDAPCnx(); ld->Wrap(info.Holder()); - ld->callback = new Nan::Callback(info[0].As()); - ld->reconnect_callback = new Nan::Callback(info[1].As()); - ld->disconnect_callback = new Nan::Callback(info[2].As()); ld->handle = NULL; + ld->tls = 0; info.GetReturnValue().Set(info.Holder()); return; @@ -129,7 +131,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { Nan::New(ldap_msgid(message)), js_result_list }; - ld->callback->Call(3, argv); + ld->result_callback->Call(3, argv); break; } case LDAP_RES_BIND: @@ -137,17 +139,17 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { case LDAP_RES_MODDN: case LDAP_RES_ADD: case LDAP_RES_DELETE: + case LDAP_RES_EXTENDED: { Local argv[] = { errparam, Nan::New(ldap_msgid(message)) }; - ld->callback->Call(2, argv); + ld->result_callback->Call(2, argv); break; } default: { - //emit an error // Nan::ThrowError("Unrecognized packet"); } @@ -158,17 +160,34 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { return; } +void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + int msgid; + + ldap_start_tls(ld->ld, NULL, NULL, &msgid); + + info.GetReturnValue().Set(msgid); +} + +void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + ldap_install_tls(ld->ld); +} + +void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_tls_inplace(ld->ld)); +} + // this fires when the LDAP lib reconnects. -// TODO: plumb in a reconnect handler -// so the caller can re-bind when the reconnect -// happens... this could be handled automatically -// (remember the last bind call) by the js driver int LDAPCnx::OnConnect(LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, struct sockaddr *addr, struct ldap_conncb *ctx) { int fd; LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; - + if (lc->handle == NULL) { lc->handle = new uv_poll_t; ldap_get_option(ld, LDAP_OPT_DESC, &fd); @@ -179,57 +198,68 @@ int LDAPCnx::OnConnect(LDAP *ld, Sockbuf *sb, } uv_poll_start(lc->handle, UV_READABLE, (uv_poll_cb)lc->Event); - lc->reconnect_callback->Call(0, NULL); + if (!lc->tls) lc->ready_callback->Call(0, NULL); return LDAP_SUCCESS; } +void LDAPCnx::OnTLSConnect(LDAP *ld, void *ssl, void *ctx, void *arg) { + LDAPCnx* lc = (LDAPCnx *)arg; + + if (lc->tls) lc->ready_callback->Call(0, NULL); +} + void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx) { // this fires when the connection closes LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; + + uv_poll_stop(lc->handle); lc->disconnect_callback->Call(0, NULL); } void LDAPCnx::Initialize(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - Nan::Utf8String url(info[0]); - int fd = 0; + Nan::Utf8String url(info[3]); int ver = LDAP_VERSION3; - int timeout = info[1]->NumberValue(); - int starttls = info[2]->NumberValue(); + int timeout = info[4]->NumberValue(); + int starttls = info[5]->NumberValue(); + int verifycert = info[6]->NumberValue(); + ld->result_callback = new Nan::Callback(info[0].As()); + ld->ready_callback = new Nan::Callback(info[1].As()); + ld->disconnect_callback = new Nan::Callback(info[2].As()); + ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); ld->ldap_callback->lc_add = OnConnect; ld->ldap_callback->lc_del = OnDisconnect; ld->ldap_callback->lc_arg = ld; if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { - Nan::ThrowError("Error init"); + int err; + ldap_get_option(ld->ld, LDAP_OPT_RESULT_CODE, &err); + Nan::ThrowError(Nan::New(ldap_err2string(err)).ToLocalChecked()); return; } struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; - ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); - ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); - ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); - ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); - - if (starttls == 1) { - ldap_start_tls_s(ld->ld, NULL, NULL); - } - - if ((ldap_simple_bind(ld->ld, NULL, NULL)) == -1) { - Nan::ThrowError("Error anon bind"); - return; - } - - ldap_get_option(ld->ld, LDAP_OPT_DESC, &fd); - - if (fd < 0) { - Nan::ThrowError("Connection issue"); - return; + ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); + ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); + ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); + ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); + + if (starttls) { + if (!verifycert) { + int val = LDAP_OPT_X_TLS_ALLOW; + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &val); + val = 0; + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &val); + } + + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_CONNECT_CB, (void *)OnTLSConnect); + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_CONNECT_ARG, ld); + ld->tls = 1; } info.GetReturnValue().Set(info.This()); @@ -256,6 +286,12 @@ void LDAPCnx::GetFD(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().Set(fd); } +void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + ldap_unbind(ld->ld); + free(ld->ldap_callback); +} + void LDAPCnx::Delete(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); Nan::Utf8String dn(info[0]); diff --git a/LDAPCnx.h b/LDAPCnx.h index 6a84f46..e1dd3de 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -7,8 +7,8 @@ class LDAPCnx : public Nan::ObjectWrap { public: static void Init(v8::Local exports); - Nan::Callback * callback; - Nan::Callback * reconnect_callback; + Nan::Callback * result_callback; + Nan::Callback * ready_callback; Nan::Callback * disconnect_callback; private: @@ -20,6 +20,7 @@ class LDAPCnx : public Nan::ObjectWrap { static void Event(uv_poll_t* handle, int status, int events); static int OnConnect(LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, struct sockaddr *addr, struct ldap_conncb *ctx); static void OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx); + static void OnTLSConnect(LDAP *ld, void *ssl, void *ctx, void *arg); static void Search(const Nan::FunctionCallbackInfo& info); static void Delete(const Nan::FunctionCallbackInfo& info); static void Bind(const Nan::FunctionCallbackInfo& info); @@ -29,11 +30,17 @@ class LDAPCnx : public Nan::ObjectWrap { static void GetErr(const Nan::FunctionCallbackInfo& info); static void GetErrNo(const Nan::FunctionCallbackInfo& info); static void GetFD(const Nan::FunctionCallbackInfo& info); + static void Close(const Nan::FunctionCallbackInfo& info); + static void StartTLS(const Nan::FunctionCallbackInfo& info); + static void InstallTLS(const Nan::FunctionCallbackInfo& info); + static void CheckTLS(const Nan::FunctionCallbackInfo& info); + + LDAP * ld; ldap_conncb * ldap_callback; uv_poll_t * handle; - + int tls; + static Nan::Persistent constructor; - LDAP * ld; }; #endif diff --git a/index.js b/index.js index 6bad64d..a5b4c0b 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ function arg(val, def) { function Stats() { this.lateresponses = 0; - this.reconnects = 0; + this.connects = 0; this.timeouts = 0; this.requests = 0; this.searches = 0; @@ -29,7 +29,7 @@ function Stats() { return this; } -function LDAP(opt) { +function LDAP(opt, fn) { this.callbacks = {}; this.defaults = { base: 'dc=com', @@ -37,19 +37,15 @@ function LDAP(opt) { scope: this.SUBTREE, attrs: '*', starttls: false, - ntimeout: 1000 + validate: true, + ntimeout: 5000, + timeout: 2000, + ready: function() {}, + disconnect: function() {} }; - this.timeout = 2000; this.stats = new Stats(); - if (typeof opt.reconnect === 'function') { - this.onreconnect = opt.reconnect; - } - if (typeof opt.disconnect === 'function') { - this.ondisconnect = opt.disconnect; - } - if (typeof opt.uri !== 'string') { throw new LDAPError('Missing argument'); } @@ -59,16 +55,32 @@ function LDAP(opt) { if (opt.scope) this.defaults.scope = opt.scope; if (opt.attrs) this.defaults.attrs = opt.attrs; if (opt.connecttimeout) this.defaults.ntimeout = opt.connecttimeout; - if (opt.starttls) this.defaults.starttls = opt.starttls; + if (opt.starttls !== undefined) this.defaults.starttls = opt.starttls; + if (opt.validate !== undefined) this.defaults.validate = opt.validate; + if (opt.ready !== undefined) this.defaults.ready = opt.ready; + if (opt.disconnect !== undefined) this.defaults.disconnect = opt.disconnect; - this.ld = new binding.LDAPCnx(this.onresult.bind(this), - this.onreconnect.bind(this), - this.ondisconnect.bind(this)); - try { - this.ld.initialize(this.defaults.uri, this.defaults.ntimeout, this.defaults.starttls); - } catch (e) { - + this.ld = new binding.LDAPCnx(); + this.ld.initialize(this.onresult.bind(this), + this.ready.bind(this), + this.ondisconnect.bind(this), + this.defaults.uri, + this.defaults.ntimeout, + this.defaults.starttls?1:0, + this.defaults.validate?1:0); + + if (opt.starttls) { + this.enqueue(this.ld.starttls(), function(err) { + if (err) return fn(err); + this.ld.installtls(); + return fn(this.ld.checktls()?undefined:new Error('TLS not active')); + }.bind(this)); + } else { + this.enqueue(this.ld.bind(null, null), function(err) { + if (fn) fn(err); + }.bind(this)); } + return this; } @@ -83,14 +95,14 @@ LDAP.prototype.onresult = function(err, msgid, data) { } }; -LDAP.prototype.onreconnect = function() { - this.stats.reconnects++; - // default reconnect callback does nothing +LDAP.prototype.ready = function() { + this.stats.connects++; + this.defaults.ready(); }; LDAP.prototype.ondisconnect = function() { this.stats.disconnects++; - // default reconnect callback does nothing + if (this.defaults.disconnect) this.defaults.disconnect(); }; LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { @@ -182,7 +194,7 @@ LDAP.prototype.close = function() { if (this.auth_connection !== undefined) { this.auth_connection.close(); } - // TODO: clean up and disconnect + return this.ld.close(); }; LDAP.prototype.enqueue = function(msgid, fn) { @@ -194,11 +206,11 @@ LDAP.prototype.enqueue = function(msgid, fn) { this.stats.errors++; return this; } - fn.timer = setTimeout(function searchTimeout() { + fn.timer = setTimeout(function requestTimeout() { delete this.callbacks[msgid]; - fn(new LDAPError('Timeout'), msgid); + fn(new LDAPError('Request Timeout'), msgid); this.stats.timeouts++; - }.bind(this), this.timeout); + }.bind(this), this.defaults.timeout); this.callbacks[msgid] = fn; this.stats.requests++; return this; diff --git a/test/certs/._.DS_Store b/test/certs/._.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..70d3a378f62df1c51be7721e15cacbc80f7635b9 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIk*Y|peR=07!nc$ zl(+@a!BBx!(Wu~P2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S0iIRqGi=7BI6 z$c1EN7Aq8`7U!21C8sK+ 0); + done(); + }); + }); +}); From 68728c50502a11f163a15ae7816148f5b87d2cdd Mon Sep 17 00:00:00 2001 From: jeremyc Date: Fri, 23 Oct 2015 17:20:07 -0600 Subject: [PATCH 05/49] Ignore fuse --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d7b4ce6..d9870ba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ test/slapd.args test/slapd.pid test/reconnect.js .DS_Store +.fuse_* From 0d9a99d83c6206568070a0657003689441470055 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 10:55:56 -0600 Subject: [PATCH 06/49] Bulletproof reconnect, TLS Support --- ._.DS_Store | Bin 0 -> 4096 bytes .gitignore | 2 + LDAPCnx.cc | 97 +++++++++++++++++++++++++------------ LDAPCnx.h | 8 ++- index.js | 110 +++++++++++++++++++++++++----------------- package.json | 1 + test/certs/device.crt | 22 +++++++++ test/certs/device.csr | 18 +++++++ test/certs/device.key | 27 +++++++++++ test/certs/rootCA.key | 30 ++++++++++++ test/certs/rootCA.pem | 27 +++++++++++ test/certs/rootCA.srl | 1 + test/index.js | 4 +- test/leakcheck | 2 +- test/slapd.conf | 11 ++++- test/tls.js | 46 ++++++++++++++++++ 16 files changed, 323 insertions(+), 83 deletions(-) create mode 100644 ._.DS_Store create mode 100644 test/certs/device.crt create mode 100644 test/certs/device.csr create mode 100644 test/certs/device.key create mode 100644 test/certs/rootCA.key create mode 100644 test/certs/rootCA.pem create mode 100644 test/certs/rootCA.srl create mode 100644 test/tls.js diff --git a/._.DS_Store b/._.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..043a46287e7cd34dd119f2330fc99df46cb7d89f GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIk*Y|peR=07!nc$ zlsFC1!BBx!(Wu~P2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S0iIRqGi=7BI6 z$c1EN7Aq8`7U!21C8sK+ exports) { Nan::SetPrototypeMethod(tpl, "modify", Modify); Nan::SetPrototypeMethod(tpl, "rename", Rename); Nan::SetPrototypeMethod(tpl, "initialize", Initialize); + Nan::SetPrototypeMethod(tpl, "abandon", Abandon); Nan::SetPrototypeMethod(tpl, "errorstring", GetErr); + Nan::SetPrototypeMethod(tpl, "close", Close); Nan::SetPrototypeMethod(tpl, "errorno", GetErrNo); Nan::SetPrototypeMethod(tpl, "fd", GetFD); + Nan::SetPrototypeMethod(tpl, "starttls", StartTLS); + Nan::SetPrototypeMethod(tpl, "installtls", InstallTLS); + Nan::SetPrototypeMethod(tpl, "checktls", CheckTLS); constructor.Reset(tpl->GetFunction()); exports->Set(Nan::New("LDAPCnx").ToLocalChecked(), tpl->GetFunction()); @@ -62,15 +67,16 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { LDAPMessage * message = NULL; LDAPMessage * entry = NULL; Local errparam; + + assert(status == 0); + int msgtype; switch(ldap_result(ld->ld, LDAP_RES_ANY, LDAP_MSG_ALL, &ldap_tv, &message)) { case 0: // timeout occurred, which I don't think happens in async mode case -1: - { // We can't really do much; we don't have a msgid to callback to break; - } default: { int err = ldap_result2error(ld->ld, message, 0); @@ -80,7 +86,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { errparam = Nan::Undefined(); } - switch ( ldap_msgtype( message ) ) { + switch ( msgtype = ldap_msgtype( message ) ) { case LDAP_RES_SEARCH_REFERENCE: break; case LDAP_RES_SEARCH_ENTRY: @@ -105,12 +111,11 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { Local js_attr_vals = Nan::New(num_vals); js_result->Set(Nan::New(attrname).ToLocalChecked(), js_attr_vals); - // char * bin = strstr(attrname, ";binary"); + // TODO: check for binary settings int bin = !strcmp(attrname, "jpegPhoto"); for (int i = 0 ; i < num_vals && vals[i] ; i++) { if (bin) { - // js_attr_vals->Set(Nan::New(i), ld->makeBuffer(vals[i])); js_attr_vals->Set(Nan::New(i), Nan::CopyBuffer(vals[i]->bv_val, vals[i]->bv_len).ToLocalChecked()); } else { js_attr_vals->Set(Nan::New(i), Nan::New(vals[i]->bv_val).ToLocalChecked()); @@ -137,6 +142,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { case LDAP_RES_MODDN: case LDAP_RES_ADD: case LDAP_RES_DELETE: + case LDAP_RES_EXTENDED: { Local argv[] = { errparam, @@ -147,7 +153,6 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { } default: { - //emit an error // Nan::ThrowError("Unrecognized packet"); } @@ -158,11 +163,6 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { return; } -// this fires when the LDAP lib reconnects. -// TODO: plumb in a reconnect handler -// so the caller can re-bind when the reconnect -// happens... this could be handled automatically -// (remember the last bind call) by the js driver int LDAPCnx::OnConnect(LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, struct sockaddr *addr, struct ldap_conncb *ctx) { @@ -188,22 +188,23 @@ void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx) { // this fires when the connection closes LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; + + uv_poll_stop(lc->handle); lc->disconnect_callback->Call(0, NULL); } void LDAPCnx::Initialize(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); Nan::Utf8String url(info[0]); - int fd = 0; int ver = LDAP_VERSION3; int timeout = info[1]->NumberValue(); - int starttls = info[2]->NumberValue(); - + int debug = info[2]->NumberValue(); + ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); ld->ldap_callback->lc_add = OnConnect; ld->ldap_callback->lc_del = OnDisconnect; ld->ldap_callback->lc_arg = ld; - + if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { Nan::ThrowError("Error init"); return; @@ -212,26 +213,11 @@ void LDAPCnx::Initialize(const Nan::FunctionCallbackInfo& info) { struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); + ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug); ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); - - if (starttls == 1) { - ldap_start_tls_s(ld->ld, NULL, NULL); - } - if ((ldap_simple_bind(ld->ld, NULL, NULL)) == -1) { - Nan::ThrowError("Error anon bind"); - return; - } - - ldap_get_option(ld->ld, LDAP_OPT_DESC, &fd); - - if (fd < 0) { - Nan::ThrowError("Connection issue"); - return; - } - info.GetReturnValue().Set(info.This()); } @@ -242,6 +228,51 @@ void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().Set(Nan::New(ldap_err2string(err)).ToLocalChecked()); } +void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_unbind(ld->ld)); +} + +void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + int msgid; + int verifycert = info[0]->NumberValue(); + int val; + int res; + + if (verifycert == 0) { + val = LDAP_OPT_X_TLS_ALLOW; + } else { + val = LDAP_OPT_X_TLS_HARD; + } + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &val); + val = 0; + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &val); + + res = ldap_start_tls(ld->ld, NULL, NULL, &msgid); + + info.GetReturnValue().Set(msgid); +} + +void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_install_tls(ld->ld)); +} + +void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_tls_inplace(ld->ld)); +} + +void LDAPCnx::Abandon(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_abandon(ld->ld, info[0]->NumberValue())); //findme +} + void LDAPCnx::GetErrNo(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); int err; @@ -268,7 +299,9 @@ void LDAPCnx::Bind(const Nan::FunctionCallbackInfo& info) { Nan::Utf8String dn(info[0]); Nan::Utf8String pw(info[1]); - info.GetReturnValue().Set(ldap_simple_bind(ld->ld, *dn, *pw)); + info.GetReturnValue().Set(ldap_simple_bind(ld->ld, + info[0]->IsUndefined()?NULL:*dn, + info[1]->IsUndefined()?NULL:*pw)); } void LDAPCnx::Rename(const Nan::FunctionCallbackInfo& info) { diff --git a/LDAPCnx.h b/LDAPCnx.h index 6a84f46..cd297ab 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -26,12 +26,18 @@ class LDAPCnx : public Nan::ObjectWrap { static void Add(const Nan::FunctionCallbackInfo& info); static void Modify(const Nan::FunctionCallbackInfo& info); static void Rename(const Nan::FunctionCallbackInfo& info); + static void Abandon(const Nan::FunctionCallbackInfo& info); static void GetErr(const Nan::FunctionCallbackInfo& info); + static void Close(const Nan::FunctionCallbackInfo& info); static void GetErrNo(const Nan::FunctionCallbackInfo& info); static void GetFD(const Nan::FunctionCallbackInfo& info); + static void StartTLS(const Nan::FunctionCallbackInfo& info); + static void InstallTLS(const Nan::FunctionCallbackInfo& info); + static void CheckTLS(const Nan::FunctionCallbackInfo& info); + ldap_conncb * ldap_callback; uv_poll_t * handle; - + static Nan::Persistent constructor; LDAP * ld; }; diff --git a/index.js b/index.js index 6bad64d..3cc11a8 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var binding = require('bindings')('LDAPCnx'); var LDAPError = require('./LDAPError'); +var _ = require('lodash'); function arg(val, def) { if (val !== undefined) { @@ -29,49 +30,59 @@ function Stats() { return this; } -function LDAP(opt) { +function LDAP(opt, fn) { this.callbacks = {}; - this.defaults = { - base: 'dc=com', - filter: '(objectClass=*)', - scope: this.SUBTREE, - attrs: '*', - starttls: false, - ntimeout: 1000 - }; - this.timeout = 2000; - this.stats = new Stats(); + this.initialconnect = true; + + this.options = _.assign({ + base: 'dc=com', + filter: '(objectClass=*)', + scope: 2, + attrs: '*', + ntimeout: 1000, + timeout: 2000, + debug: 0, + starttls: false, + validatecert: true, + connect: function() {}, + disconnect: function() {}, + ready: fn + }, opt); - if (typeof opt.reconnect === 'function') { - this.onreconnect = opt.reconnect; - } - if (typeof opt.disconnect === 'function') { - this.ondisconnect = opt.disconnect; - } if (typeof opt.uri !== 'string') { throw new LDAPError('Missing argument'); } - this.defaults.uri = opt.uri; - if (opt.base) this.defaults.base = opt.base; - if (opt.filter) this.defaults.filter = opt.filter; - if (opt.scope) this.defaults.scope = opt.scope; - if (opt.attrs) this.defaults.attrs = opt.attrs; - if (opt.connecttimeout) this.defaults.ntimeout = opt.connecttimeout; - if (opt.starttls) this.defaults.starttls = opt.starttls; this.ld = new binding.LDAPCnx(this.onresult.bind(this), - this.onreconnect.bind(this), + this.onconnect.bind(this), this.ondisconnect.bind(this)); try { - this.ld.initialize(this.defaults.uri, this.defaults.ntimeout, this.defaults.starttls); + this.ld.initialize(this.options.uri, this.options.ntimeout, this.options.debug); } catch (e) { - + //TODO: does init still need to throw? + } + + if (this.options.starttls) { + this.enqueue(this.ld.starttls(this.options.validatecert), function(err) { + if (err) return this.options.ready(err); + if (err = this.ld.installtls() !== 0) return this.options.ready(new LDAPError(this.ld.errorstring())); + if (err = this.ld.checktls() !== 1) return this.options.ready(new LDAPError('Expected TLS')); + return this.enqueue(this.ld.bind(undefined, undefined), this.do_ready.bind(this)); + }.bind(this)); + } else { + this.enqueue(this.ld.bind(undefined, undefined), this.do_ready.bind(this)); } + return this; } +LDAP.prototype.do_ready = function(err) { + this.initialconnect = false; + if (typeof this.options.ready === 'function') this.options.ready(err); +}; + LDAP.prototype.onresult = function(err, msgid, data) { this.stats.results++; if (this.callbacks[msgid]) { @@ -83,14 +94,15 @@ LDAP.prototype.onresult = function(err, msgid, data) { } }; -LDAP.prototype.onreconnect = function() { +LDAP.prototype.onconnect = function() { this.stats.reconnects++; - // default reconnect callback does nothing + if (this.initialconnect) return; // suppress initial connect event + this.options.connect(); }; LDAP.prototype.ondisconnect = function() { this.stats.disconnects++; - // default reconnect callback does nothing + this.options.disconnect(); }; LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { @@ -124,10 +136,10 @@ LDAP.prototype.add = function(dn, attrs, fn) { LDAP.prototype.search = function(opt, fn) { this.stats.searches++; - return this.enqueue(this.ld.search(arg(opt.base , this.defaults.base), - arg(opt.filter , this.defaults.filter), - arg(opt.attrs , this.defaults.attrs), - arg(opt.scope , this.defaults.scope)), fn); + 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)), fn); }; LDAP.prototype.rename = function(dn, newrdn, fn) { @@ -166,7 +178,7 @@ LDAP.prototype.findandbind = function(opt, fn) { return; } if (this.auth_connection === undefined) { - this.auth_connection = new LDAP(this.defaults); + this.auth_connection = new LDAP(this.options); } this.auth_connection.bind({ binddn: data[0].dn, password: opt.password }, function(err) { if (err) { @@ -182,32 +194,40 @@ LDAP.prototype.close = function() { if (this.auth_connection !== undefined) { this.auth_connection.close(); } - // TODO: clean up and disconnect + this.ld.close(); + this.ld = undefined; }; LDAP.prototype.enqueue = function(msgid, fn) { - if (msgid == -1) { + if (msgid == -1 || this.ld === undefined) { + if (this.ld.errorstring() === 'Can\'t contact LDAP server') { + Object.keys(this.callbacks).forEach(function(msgid) { + this.callbacks[msgid](new LDAPError('Timeout')); + delete this.callbacks[msgid]; + this.ld.abandon(msgid); + }.bind(this)); + } process.nextTick(function() { fn(new LDAPError(this.ld.errorstring())); - return; }.bind(this)); this.stats.errors++; return this; } fn.timer = setTimeout(function searchTimeout() { + this.ld.abandon(msgid); delete this.callbacks[msgid]; - fn(new LDAPError('Timeout'), msgid); + fn(new LDAPError('Timeout')); this.stats.timeouts++; - }.bind(this), this.timeout); + }.bind(this), this.options.timeout); this.callbacks[msgid] = fn; this.stats.requests++; return this; }; -LDAP.prototype.BASE = 0; -LDAP.prototype.ONELEVEL = 1; -LDAP.prototype.SUBTREE = 2; -LDAP.prototype.SUBORDINATE = 3; -LDAP.prototype.DEFAULT = 4; +LDAP.BASE = 0; +LDAP.ONELEVEL = 1; +LDAP.SUBTREE = 2; +LDAP.SUBORDINATE = 3; +LDAP.DEFAULT = 4; module.exports = LDAP; diff --git a/package.json b/package.json index 941075d..516dd23 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "bindings": "^1.2.1", + "lodash": "^3.10.1", "nan": "^2.0.5", "node-gyp": "^1.0.3" }, diff --git a/test/certs/device.crt b/test/certs/device.crt new file mode 100644 index 0000000..1a14d76 --- /dev/null +++ b/test/certs/device.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnDCCAoQCCQDpkoraKTv49zANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC +Q0ExCzAJBgNVBAgTAk5UMRQwEgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChME +RGVtbzENMAsGA1UECxMERGVtbzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20x +IzAhBgkqhkiG9w0BCQEWFGplcmVteWNAc3NpbWljcm8uY29tMB4XDTE1MTAyMjE1 +NTcxOFoXDTI5MDYzMDE1NTcxOFowgY8xCzAJBgNVBAYTAkNBMQswCQYDVQQIEwJO +VDEUMBIGA1UEBxMLWWVsbG93a25pZmUxDTALBgNVBAoTBERlbW8xDTALBgNVBAsT +BERlbW8xGjAYBgNVBAMTEWRlbW8uc3NpbWljcm8uY29tMSMwIQYJKoZIhvcNAQkB +FhRqZXJlbXljQHNzaW1pY3JvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMBx7klo3CxR5vvFxf0Su58PXQjFe5IEc3p0HKXsOHNVOchIy1raU0+O +RpBFX+e/XkNPjMi/0Y4TKLiwxKVW7KtBBltBRx+2UjuY4qIWAZJQSGcq6qNAtzms +tQP2HWOhSeFFHoW1NXK88HYo7KDVIAD135cUSvn5+jqiwGYe0rX/lBUkOCmPQu6/ +LyzBDgRVsrZOUzGdgsWjhQQFQSPM6LlgOzCkj1oCGgaO8C7/9D1p+f2ACP5zTcE+ +JZ3Sn1ry10IK58RBAR0tQnX6o06cSlLzxNbj5/Zl2rA/r0nB8ZN/iILbas440V+h +DPPxo1irBsW9TsElA5JWHi/KXBXfZSsCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA +sd3QR94dPIPpi+EmkD9pKuLu6UTTQXe49QaqdZ1zbmzcm5I446Mnca5QbwrjR1HJ +mLyQ7vUeIqBWwJTmXnKS7A0ZjeSXy1r4mC8oHdyjF/2xgYXPltsaKUjn+qBUo/ID +QgOAREfn+sR3hoqUsHFCohW6mO4ZLartUNRlliNWWATaq60SB5AmMDe9UixSq5xq +9i073cNmnWUcIJ/ApWh5jS6FlHL7P7tBdWXR4+yud9+18khdeab3HW7diFGTNsvU +XirNk7tjReltkgPqfRcCe9gv0QVgy31aK0eBNvt15IiT3jhQdEC1W3TyvId3MhTa +xNzjR8MXrASMZbIve6tFQw== +-----END CERTIFICATE----- diff --git a/test/certs/device.csr b/test/certs/device.csr new file mode 100644 index 0000000..8d3fa79 --- /dev/null +++ b/test/certs/device.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC1TCCAb0CAQAwgY8xCzAJBgNVBAYTAkNBMQswCQYDVQQIEwJOVDEUMBIGA1UE +BxMLWWVsbG93a25pZmUxDTALBgNVBAoTBERlbW8xDTALBgNVBAsTBERlbW8xGjAY +BgNVBAMTEWRlbW8uc3NpbWljcm8uY29tMSMwIQYJKoZIhvcNAQkBFhRqZXJlbXlj +QHNzaW1pY3JvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMBx +7klo3CxR5vvFxf0Su58PXQjFe5IEc3p0HKXsOHNVOchIy1raU0+ORpBFX+e/XkNP +jMi/0Y4TKLiwxKVW7KtBBltBRx+2UjuY4qIWAZJQSGcq6qNAtzmstQP2HWOhSeFF +HoW1NXK88HYo7KDVIAD135cUSvn5+jqiwGYe0rX/lBUkOCmPQu6/LyzBDgRVsrZO +UzGdgsWjhQQFQSPM6LlgOzCkj1oCGgaO8C7/9D1p+f2ACP5zTcE+JZ3Sn1ry10IK +58RBAR0tQnX6o06cSlLzxNbj5/Zl2rA/r0nB8ZN/iILbas440V+hDPPxo1irBsW9 +TsElA5JWHi/KXBXfZSsCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4IBAQA8LNE65w+r +zBLxvZ64o2xSDS3QAFox6sEXCDSCe/0ExJ56TzvaGbUET9HnlDrHcOrWEyIVxkf0 +Ifyyzz0akpNYcBSfY5cckipmIIBSVcXYVGTDRJ/pdls58Nh+CMXMkR+PQ5dBvNBK +GTh/MVLGTYdpvDw0gEprqi3VevYkEtg2QpLt/AfKiHMOkZ8F5lo+oRF+D/GJmt5r +2tZDfJVWgoYlkMtRRuJZUOQAp9XFwl+K96/MLh/IlY41RbzQNyG898PRRfslTXB1 +dmT56IIuLz47fS7Dxd0XqzpE7QJUeJXKZGwvthZc6C8k2lH23dOWvLqHsaY3VfZL +36wOVxdY4PR+ +-----END CERTIFICATE REQUEST----- diff --git a/test/certs/device.key b/test/certs/device.key new file mode 100644 index 0000000..6a45f42 --- /dev/null +++ b/test/certs/device.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwHHuSWjcLFHm+8XF/RK7nw9dCMV7kgRzenQcpew4c1U5yEjL +WtpTT45GkEVf579eQ0+MyL/RjhMouLDEpVbsq0EGW0FHH7ZSO5jiohYBklBIZyrq +o0C3Oay1A/YdY6FJ4UUehbU1crzwdijsoNUgAPXflxRK+fn6OqLAZh7Stf+UFSQ4 +KY9C7r8vLMEOBFWytk5TMZ2CxaOFBAVBI8zouWA7MKSPWgIaBo7wLv/0PWn5/YAI +/nNNwT4lndKfWvLXQgrnxEEBHS1CdfqjTpxKUvPE1uPn9mXasD+vScHxk3+Igttq +zjjRX6EM8/GjWKsGxb1OwSUDklYeL8pcFd9lKwIDAQABAoIBAQCSKrazGSsJmpeX +KWsswbqxoCiojd5CVJElM+XCfH2P0+6UWf3inqriZQzhbV/flHFTLKugmlje0Vx/ +kvt5HWGa3UOnshgEVSV2ULPqKk69Q68KdQVMQ84mxy+ht6Aw2QNVT3tUUQMsh6cY +CBNaQSYStK1Dgc1EuoI9YPpDVivywL+2TCUDhSzyTOlmuN71eVJVJ5z5lEVRTcjH +kZhyojJbc4bOVWNtd04E0lINYb47Nw5y42Dbl3hzXEHjbxDtqwaH/8zCr514UKwb +R0sP2qbZGhW3D8SKFobqFKBioO5RIOBaLvAN5IbmgjNNelk3jKVrNireczbRRY7t +6pGEfi4RAoGBAOj4R414Z5yEM3z5IGctOKnNlnvqV1t8OuAn1admhxOpFQmEpdsy +FgO3dQ2i1wVomWJFnf05nLqnhMs5RInOPt4X5FPuL3O9FQpRfo7la0JGxF0ILWyY +dpIsBhFvBFKX/KcklX+TU/Pvw/6sj0H2vb+KadNrCZo4F06vDgGQM4JJAoGBANN4 +GTKR9PQnhg5LAYVFQ27W8cUzMyvhr3t9DhrA/4NQNfPO5NdUSVyzIScO37RjHlB/ +yjRiATGkhz0xWidxef716tVNpSNpH/exBL8UGmTNPwp89Uy/N9mgYo/yVwugcGor +iqxvh2s7kyHVfZffWoEN9Q1I28LbkqejNNB8QuvTAoGAOt7qrew8OogJvs3xi0EZ +LYefPGcGdj7ZXeWTDv9QqP40K7iSdOaeO4gzkyOQNHSvNe8jsmbJnT1RyE0Lbctp +hZQCBdeNtDCWzYm0coW06gWZ/2xeli+c3ukzC1rDe9+eX9pV0Ow47c6r94JBnUit +wGZIwb0tqwP7l82Su4BmE8kCgYAo92MqQMxLYDzAGBe7Uae2mT1NDpYjMh1ktt08 +oZbeQXOyP6plbJaptqn9fwwnTexZe+gYLcQ9cbohSKZGbd1MXyeXGuua6Iqg2VIq +EiLq1DgaOArtSz3ukvuFF1V1kycz6it7LD/3rhrauxkRittllOacJDkujorinuNk +YC42sQKBgQDNY2eCtKMC7Lf8Lm5jnbNdW7s3iGSkeHBrxLf6+5JaV9ANHPU2DC23 +rUecryszi/mIePeEYbbSiqxSa/2rbIS8s4WkNRpXENWRpACaOeRzNLhxbL5kFKKh +aiiK/+rGS+T1KDSoTm5VYsyj1MG0bRfIdGhrnvCapDgTBDchItF82w== +-----END RSA PRIVATE KEY----- diff --git a/test/certs/rootCA.key b/test/certs/rootCA.key new file mode 100644 index 0000000..5e669d1 --- /dev/null +++ b/test/certs/rootCA.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,22673A99BDDF38E8 + +ffN8zTCH8u/KHRSdxR8KrcXMKkN3laq5e4gANucNBznvj7Nx4mCHi82ggdc/KRu9 +lSS90qb7Mkcd9bJZ9WiJF+JnaShAGlLH0vyfwYN1EniSCx6HlqPCGgBt/w6bU97o +HLPRsWHXIVf/LZ+YcB8X9Z0e/ookZqBHsbsGb2+uBX7EDtok6P6K+wYR1ousppDA +3cTp42e4U2egNt1bUyYFnfC3+p5Rci7wHopcrDgouEP8NeSqM7Jhrtl59Kl0eHvV +nI2G//5asbvlLz1CcY+HkJ3acJbsiUwxXQtUcLmAytgKiyJ6Mc8tF8e2hpbLmGJZ +y0QY/Tc2eUXAX//nxnlyAT1YGghAORnxyQU6+lXvqk/9qLq/fEaT9GhcK1bVqCxU +vkMKZx9WleetuESgpo83J/RrOdoToQL0ngyd311ivmuFUg7RaJLAPVtxiz3nydxN +2QXkKaGw4UOnEgkDVGyzIfJLqTuuiluFuvAPx1DhbdIe7schbM5AIbpkEtnJIVU1 +QMxZ/P51rjJlBOqELQBEiD63dZ6J7MBX7zzuEOLF8SW1RyuA5y2f8O0n/DubqapH +ZC0uZGfG6C/Auy+DY/wrAfFsgA4DBGUdX3/iXg0SXJcwIwOOyJdUAgWbtNtMx0vu +qOb1vu28UMfns6pPi64DWSARy4pDkpPKsPnakQ7M87sOiaXSM259RAwJhMoWJwcc +xPkWOsF7hKS1Jy/ZVfZcbIF4Fs6m6Zo1oUi35blVH5QfKlLujP8jlaF7UgFovI7w +1zoJ6JU99ZAFT3gA9GOQYIWEDq+3MCnOmqU+JlNynsrOr7kF9PkoewQuzcJeA0/n +MP1O6dCVmqdBngt1nAHTyXjiKQj5WmFsoaAhzl5daOL0fSTcBnDXBqTjPZl8l3Iy +FN5r7pVYyggWCgHoMQiV0zUUAb80jiLNaHULjhUakeKbVIOTagPDpy36K3xRrFz2 +1cM1XpJKfTaN9Rovf6+BRr6ecqUHVStdgusAW5VErSsYmZhz5KuyYJeZwFnZR7uP +SPCD8QwBsLpf3t9h2UoBe/GTKZcajnNv6nZ/ld5YkPa2G+BMZlg5/wNhhfdc5vjV +czeixVl3iOFn5zwbUCPZq1oxXkgT5HExwWGqKtAUyjg2O4tWUDshJX7vwlQ8PEJ5 +9Fy61ZWlzY1xTzYIh3AzQAHHSWVqt6cuOXITlTJGCON02OHgJ56NOe/Ci7/XWyoI +k+SQ2dvPjoaQL5r7HS6fmRO+VlcugB3zSBiTZTCv4+z1/cUVT8HWH6PNV2cDAxx7 +HCGmTPe3WFJZxKTxGJ6x1NKRsyz6a5Uk3BzB5HVG5av3sXlTDy6dNI5oXHtZ1ND5 +2M2KNC9bif8RqrUXrhbrTLbIYIog6JxJcdJsxxkhTInQs7P5/xEvKxosHkxdWTM7 +UVH4c/5WRn35l2jTUe4QIKWokrCDggpbyh+qMLo7AEdUQ72raXZ3MxKD9tXL82xh +uNV+vG0ojsy4zjFBEGqlbchDShfOVzIZBRV2Q7yAKWJ9h6Q2sTl2CQG8obG73b2K +QDJ6jw2H3BeaBXnWdbl3QqhPVPCA4Tl8I2egkujPILUkDLVsLeq95g== +-----END RSA PRIVATE KEY----- diff --git a/test/certs/rootCA.pem b/test/certs/rootCA.pem new file mode 100644 index 0000000..1bd4035 --- /dev/null +++ b/test/certs/rootCA.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmzCCA4OgAwIBAgIJAL6wGd1s4F9TMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYD +VQQGEwJDQTELMAkGA1UECBMCTlQxFDASBgNVBAcTC1llbGxvd2tuaWZlMQ0wCwYD +VQQKEwREZW1vMQ0wCwYDVQQLEwREZW1vMRowGAYDVQQDExFkZW1vLnNzaW1pY3Jv +LmNvbTEjMCEGCSqGSIb3DQEJARYUamVyZW15Y0Bzc2ltaWNyby5jb20wHhcNMTUx +MDIyMTU1NjAwWhcNMTgwODExMTU1NjAwWjCBjzELMAkGA1UEBhMCQ0ExCzAJBgNV +BAgTAk5UMRQwEgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChMERGVtbzENMAsG +A1UECxMERGVtbzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20xIzAhBgkqhkiG +9w0BCQEWFGplcmVteWNAc3NpbWljcm8uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA0s3y2/WY1ZEtsU6/5UwRCZKsf88ApctqB3P5aTB9Ow53AOLF +hj/oN/cT2qfFtPAp0jKtUS9/bROQbsy0tzRc9OBDZ5qc7XZhlXikcPAN16esmA7j +uyNdQ6wgfX9GVdOywQKONEqePvg+SX9xiq5TrulDfHF2IS+G1UJRWkACuGSaTXhb +v5CCQceSvPipRZts+7SMERkgciCH2oVuyGs6n7Sc1LGmNtq7FsQgTs8RvtgEJ+eV +SkGqiy4/59evohRg2fSos/kfQFGMvYyYj4EDe8spnGOa919wU5z+16Oog/VOB/jv +V3CpRuegD2R9at1Rc8XGb+xpwn0JtjTbYDEQnQIDAQABo4H3MIH0MB0GA1UdDgQW +BBQr9y+Kq4lHtM0ELtphzSkA8ck8PjCBxAYDVR0jBIG8MIG5gBQr9y+Kq4lHtM0E +LtphzSkA8ck8PqGBlaSBkjCBjzELMAkGA1UEBhMCQ0ExCzAJBgNVBAgTAk5UMRQw +EgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChMERGVtbzENMAsGA1UECxMERGVt +bzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20xIzAhBgkqhkiG9w0BCQEWFGpl +cmVteWNAc3NpbWljcm8uY29tggkAvrAZ3WzgX1MwDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQUFAAOCAQEAhOxS1ti8/X+neasbkX0x6k+3cQ7cVmzuyALJbn+smotG +kjFK0ulY/zAYhnAvLQBu625vHugW1UMIvXxpJBFOS5x/O8+B07FweJxvqclF1xcG +A481xXuMcPQEvcysjY/6rJbo8PRVydCegZTWwy7PgA30gmouzLkSUkRgamcZftqR +xjkQYvFvQ9YkIMLgZedpikLZ/9rp60udzAyN44FPfhGVqgIYu2wxtAnfaIYtLOgf +KTQorr6PMIlOmhGu9QGGPsTen2QRukbk48isuDCV6JXyHtDmJQrsyjc61yc1sh7e +ZfH1tBk4OUCauZIH9Pk+WfpFkbyjWJDSBVqsQGBuvQ== +-----END CERTIFICATE----- diff --git a/test/certs/rootCA.srl b/test/certs/rootCA.srl new file mode 100644 index 0000000..3835be0 --- /dev/null +++ b/test/certs/rootCA.srl @@ -0,0 +1 @@ +E9928ADA293BF8F7 diff --git a/test/index.js b/test/index.js index 15b5ea4..0ca9e84 100644 --- a/test/index.js +++ b/test/index.js @@ -19,13 +19,13 @@ function showImage(what) { } -describe('LDAP', function() { +describe('LDAP', function(done) { it ('Should initialize OK', function() { ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*' - }); + }, done); }); it ('Should search', function(done) { ldap.search({ diff --git a/test/leakcheck b/test/leakcheck index 5173f7e..4f270ac 100644 --- a/test/leakcheck +++ b/test/leakcheck @@ -7,7 +7,7 @@ var assert = require('assert'); var ldap; var errors = {};; -ldap = new LDAP({uri: 'ldap://10.0.0.1:1234', connecttimeout: 100}); +ldap = new LDAP({uri: 'ldap://localhost:1234', connecttimeout: 1000}); setInterval(function() { ldap.search({ base: 'dc=sample,dc=com', diff --git a/test/slapd.conf b/test/slapd.conf index b0e3a56..78f1fe2 100644 --- a/test/slapd.conf +++ b/test/slapd.conf @@ -16,10 +16,17 @@ argsfile ./slapd.args # Load dynamic backend modules: modulepath /usr/local/libexec/openldap -moduleload back_bdb +moduleload back_mdb # moduleload back_hdb # moduleload back_ldap +timelimit 10 + +TLSCACertificateFile certs/rootCA.pem +TLSCertificateFile certs/device.crt +TLSCertificateKeyFile certs/device.key + + # Sample security restrictions # Require integrity protection (prevent hijacking) # Require 112-bit (3DES or better) encryption for updates @@ -53,7 +60,7 @@ idletimeout 100 # BDB database definitions ####################################################################### -database bdb +database mdb # overlay syncprov # syncprov-checkpoint 10 10 diff --git a/test/tls.js b/test/tls.js new file mode 100644 index 0000000..57d7211 --- /dev/null +++ b/test/tls.js @@ -0,0 +1,46 @@ +/*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ + +'use strict'; + +var LDAP = require('../'); +var assert = require('assert'); +var fs = require('fs'); +var ldap; + +describe('LDAP TLS', function() { + it ('Should NOT initialize OK', function(done) { + ldap = new LDAP({ + uri: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*', + starttls: true + }, function(err) { + assert.ifError(!err); + done(); + }); + }); + it ('Should initialize OK', function(done) { + ldap = new LDAP({ + uri: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*', + starttls: true, + validatecert: false + }, function(err) { + assert.ifError(err); + done(); + }); + }); + it ('Should search via TLS', function(done) { + ldap.search({ + filter: '(cn=babs)', + scope: LDAP.SUBTREE + }, function(err, res) { + assert.ifError(err); + assert.equal(res.length, 1); + assert.equal(res[0].sn[0], 'Jensen'); + assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); + done(); + }); + }); +}); From 96d9c66c1f130e2637e109e1252bb03abc5995ce Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 11:10:38 -0600 Subject: [PATCH 07/49] Fix merge. --- test/tls.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/test/tls.js b/test/tls.js index 35581fc..4efdec0 100644 --- a/test/tls.js +++ b/test/tls.js @@ -8,52 +8,27 @@ var fs = require('fs'); var ldap; describe('LDAP TLS', function() { -<<<<<<< HEAD - it ('Should NOT initialize OK', function(done) { -======= it ('Should fail TLS on cert validation', function(done) { - this.timeout(5000); ->>>>>>> master ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*', -<<<<<<< HEAD starttls: true }, function(err) { assert.ifError(!err); done(); }); }); - it ('Should initialize OK', function(done) { -======= - starttls: true, - validate: true, - timeout: 10000 - }, function(err) { - assert(err); - ldap.close(); - done(); - }); - }); it ('Should connect', function(done) { this.timeout(10000); ->>>>>>> master ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*', starttls: true, -<<<<<<< HEAD validatecert: false }, function(err) { assert.ifError(err); -======= - validate: false, - timeout: 10000 - }, function(err) { - assert(!err); ->>>>>>> master done(); }); }); @@ -61,17 +36,11 @@ describe('LDAP TLS', function() { ldap.search({ filter: '(cn=babs)', scope: LDAP.SUBTREE -<<<<<<< HEAD }, function(err, res) { assert.ifError(err); assert.equal(res.length, 1); assert.equal(res[0].sn[0], 'Jensen'); assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); -======= - }, function(err, data) { - assert(data); - assert(data.length > 0); ->>>>>>> master done(); }); }); From a4b9ce6e8eda61d1c86faf20995464d538b20f6c Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 13:16:58 -0600 Subject: [PATCH 08/49] findandbind now works over TLS --- index.js | 52 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 3cc11a8..a6801c6 100644 --- a/index.js +++ b/index.js @@ -46,10 +46,10 @@ function LDAP(opt, fn) { starttls: false, validatecert: true, connect: function() {}, - disconnect: function() {}, - ready: fn + disconnect: function() {} }, opt); + this.readyfn = fn; if (typeof opt.uri !== 'string') { throw new LDAPError('Missing argument'); @@ -63,12 +63,11 @@ function LDAP(opt, fn) { } catch (e) { //TODO: does init still need to throw? } - if (this.options.starttls) { this.enqueue(this.ld.starttls(this.options.validatecert), function(err) { - if (err) return this.options.ready(err); - if (err = this.ld.installtls() !== 0) return this.options.ready(new LDAPError(this.ld.errorstring())); - if (err = this.ld.checktls() !== 1) return this.options.ready(new LDAPError('Expected TLS')); + if (err) return this.readyfn(err); + if ((err = this.ld.installtls()) !== 0) return this.readyfn(new LDAPError(this.ld.errorstring())); + if ((err = this.ld.checktls()) !== 1) return this.readyfn(new LDAPError('Expected TLS')); return this.enqueue(this.ld.bind(undefined, undefined), this.do_ready.bind(this)); }.bind(this)); } else { @@ -80,7 +79,7 @@ function LDAP(opt, fn) { LDAP.prototype.do_ready = function(err) { this.initialconnect = false; - if (typeof this.options.ready === 'function') this.options.ready(err); + if (typeof this.readyfn === 'function') this.readyfn(err); }; LDAP.prototype.onresult = function(err, msgid, data) { @@ -105,6 +104,10 @@ LDAP.prototype.ondisconnect = function() { this.options.disconnect(); }; +LDAP.prototype.tlsactive = function() { + return this.ld.checktls(); +}; + LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { this.stats.removes++; if (typeof dn !== 'string' || @@ -169,27 +172,27 @@ LDAP.prototype.findandbind = function(opt, fn) { } this.search(opt, function(err, data) { - if (err) { - fn(err); - return; - } + if (err) return fn(err); + if (data === undefined || data.length != 1) { - fn(new LDAPError('Search returned ' + data.length + ' results, expected 1')); - return; + return fn(new LDAPError('Search returned ' + data.length + ' results, expected 1')); } if (this.auth_connection === undefined) { - this.auth_connection = new LDAP(this.options); + this.auth_connection = new LDAP(this.options, function(err) { + if (err) return fn(err); + return this.authbind(data[0].dn, opt.password, fn); + }.bind(this)); + } else { + this.authbind(data[0].dn, opt.password, fn); } - this.auth_connection.bind({ binddn: data[0].dn, password: opt.password }, function(err) { - if (err) { - fn(err); - return; - } - fn(undefined, data[0]); - }.bind(this)); + 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(); @@ -201,6 +204,13 @@ LDAP.prototype.close = function() { 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.callbacks).forEach(function(msgid) { this.callbacks[msgid](new LDAPError('Timeout')); delete this.callbacks[msgid]; From 7dd64d280823739210b9dbe8e8bad681f1651dca Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 13:17:15 -0600 Subject: [PATCH 09/49] Better error handling from assert() --- README.md | 2 -- test/index.js | 29 +++++++++++++++-------------- test/tls.js | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 75a55bc..af4a174 100644 --- a/README.md +++ b/README.md @@ -351,5 +351,3 @@ Basically, these are features I don't really need myself. * Referral chasing * Binary attribute handling * Paged search results -* close() and friends -* test starttls diff --git a/test/index.js b/test/index.js index 266fe4d..f76bdc3 100644 --- a/test/index.js +++ b/test/index.js @@ -33,7 +33,7 @@ describe('LDAP', function(done) { filter: '(cn=babs)', scope: LDAP.SUBTREE }, function(err, res) { - assert.equal(err, undefined); + assert.ifError(err); assert.equal(res.length, 1); assert.equal(res[0].sn[0], 'Jensen'); assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); @@ -54,6 +54,7 @@ describe('LDAP', function(done) { filter: '(cn=albert)', attrs: 'sn' }, function(err, res) { + assert.ifError(err); assert.notEqual(res, undefined); assert.notEqual(res[0], undefined); assert.equal(res[0].sn[0], 'Root'); @@ -73,7 +74,7 @@ describe('LDAP', function(done) { }); it ('Should not delete', function(done) { ldap.delete('cn=Albert,ou=Accounting,dc=sample,dc=com', function(err) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); @@ -84,7 +85,7 @@ describe('LDAP', function(done) { attrs: '*', password: 'foobarbaz' }, function(err, data) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); }); @@ -95,7 +96,7 @@ describe('LDAP', function(done) { attrs: '*', password: 'foobarbaz' }, function(err, data) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); }); @@ -106,25 +107,25 @@ describe('LDAP', function(done) { attrs: 'cn', password: 'foobarbax' }, function(err, data) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); it ('Should not bind', function(done) { ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'xsecret'}, function(err) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); it ('Should bind', function(done) { ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'secret'}, function(err) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); }); it ('Should delete', function(done) { ldap.delete('cn=Albert,ou=Accounting,dc=sample,dc=com', function(err) { - assert.equal(err, undefined); + assert.ifError(err); ldap.search({ base: 'dc=sample,dc=com', filter: '(cn=albert)', @@ -154,7 +155,7 @@ describe('LDAP', function(done) { vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] } ], function(err, res) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); @@ -178,7 +179,7 @@ describe('LDAP', function(done) { vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] } ], function(err, res) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); @@ -200,16 +201,16 @@ describe('LDAP', function(done) { }); it ('Should rename', function(done) { ldap.rename('cn=Albert,ou=Accounting,dc=sample,dc=com', 'cn=Alberto', function(err) { - assert.equal(err, undefined); + assert.ifError(err); ldap.rename('cn=Alberto,ou=Accounting,dc=sample,dc=com', 'cn=Albert', function(err) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); }); }); it ('Should fail to rename', function(done) { ldap.rename('cn=Alberto,ou=Accounting,dc=sample,dc=com', 'cn=Albert', function(err) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); @@ -218,7 +219,7 @@ describe('LDAP', function(done) { { op: 'add', attr: 'title', vals: [ 'King of Callbacks' ] }, { op: 'add', attr: 'telephoneNumber', vals: [ '18005551212', '18005551234' ] } ], function(err) { - assert(!err); + assert.ifError(err); ldap.search( { base: 'dc=sample,dc=com', diff --git a/test/tls.js b/test/tls.js index 4efdec0..4b30eb7 100644 --- a/test/tls.js +++ b/test/tls.js @@ -44,4 +44,29 @@ describe('LDAP TLS', function() { done(); }); }); + it ('Should findandbind()', function(done) { + ldap.findandbind({ + base: 'dc=sample,dc=com', + filter: '(cn=Charlie)', + attrs: '*', + password: 'foobarbaz' + }, function(err, data) { + assert.ifError(err); + done(); + }); + }); + it ('Should fail findandbind()', function(done) { + ldap.findandbind({ + base: 'dc=sample,dc=com', + filter: '(cn=Charlie)', + attrs: 'cn', + password: 'foobarbax' + }, function(err, data) { + assert.ifError(!err); + done(); + }); + }); + it ('Should still have TLS', function() { + assert(ldap.tlsactive()); + }); }); From ea2b0ea36aebeb8616ac2707ccbb38edb9f226da Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 14:11:12 -0600 Subject: [PATCH 10/49] Named callback functions. --- index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index a6801c6..7900003 100644 --- a/index.js +++ b/index.js @@ -64,7 +64,7 @@ function LDAP(opt, fn) { //TODO: does init still need to throw? } if (this.options.starttls) { - this.enqueue(this.ld.starttls(this.options.validatecert), function(err) { + this.enqueue(this.ld.starttls(this.options.validatecert), function tlsStarted(err) { if (err) return this.readyfn(err); if ((err = this.ld.installtls()) !== 0) return this.readyfn(new LDAPError(this.ld.errorstring())); if ((err = this.ld.checktls()) !== 1) return this.readyfn(new LDAPError('Expected TLS')); @@ -171,14 +171,14 @@ LDAP.prototype.findandbind = function(opt, fn) { throw new Error('Missing argument'); } - this.search(opt, function(err, data) { + 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(err) { + this.auth_connection = new LDAP(this.options, function newAuthConnection(err) { if (err) return fn(err); return this.authbind(data[0].dn, opt.password, fn); }.bind(this)); @@ -211,13 +211,13 @@ LDAP.prototype.enqueue = function(msgid, fn) { // 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.callbacks).forEach(function(msgid) { + Object.keys(this.callbacks).forEach(function fireTimeout(msgid) { this.callbacks[msgid](new LDAPError('Timeout')); delete this.callbacks[msgid]; this.ld.abandon(msgid); }.bind(this)); } - process.nextTick(function() { + process.nextTick(function emitError() { fn(new LDAPError(this.ld.errorstring())); }.bind(this)); this.stats.errors++; From e7ea0b6557f21e21e5deb6f59434f8c8fe4c09af Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 27 Oct 2015 14:41:22 -0600 Subject: [PATCH 11/49] Updated README with new API details --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index af4a174..0d56557 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ldap-client 2.X.X +ldap-client 3.X.X =============== OpenLDAP client bindings for Node.js. Requires libraries from @@ -6,7 +6,7 @@ http://www.openldap.org installed. Now uses Nan to ensure it will build for all version of Node.js. -This release is a complete rewrite from 1.x.x, but remains API compatible. +This is an API-breaking release, but it should be easy to convert to the new API. NOTE: The module has been renamed to `ldap-client` as `npm` no longer accepts capital letters. @@ -39,11 +39,16 @@ To install the latest release from npm: You will also require the LDAP Development Libraries (on Ubuntu, `sudo apt-get install libldap2-dev`) +Reconnection +========== +If the connection fails during operation, the client library will handle the reconnection, calling the function specified in the reconnect option. This callback is a good place to put bind()s and other things you want to always be in place. + +You must close() the instance to stop the reconnect behavior. API === - new LDAP(options); + new LDAP(options, readyCallback); Options are provided as a JS object: @@ -53,13 +58,16 @@ var LDAP = require('ldap-client'); var ldap = new LDAP({ uri: 'ldap://server', // string starttls: false, // boolean, default is false + validatecert: false, // Verify server certificate connecttimeout: -1, // seconds, default is -1 (infinite timeout), connect timeout base: 'dc=com', // default base for all future searches - attrs: '*', // default attribute list for all future searches + attrs: '*', // default attribute list for future searches filter: '(objectClass=*)', // default filter for all future searches scope: LDAP.SUBTREE, // default scope for all future searches reconnect: function(), // optional function to call when connect/reconnect occurs disconnect: function(), // optional function to call when disconnect occurs +}, function(err) { + // connected and ready }); ``` @@ -150,6 +158,7 @@ mine). The exception to this rule is the 'dn' attribute - this is always a single-valued string. Example of search result: + ```js [ { gidNumber: [ '2000' ], objectClass: [ 'posixAccount', 'top', 'account' ], From 62ed272f972bad5c7c386ed4996a0f5f6b0289da Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 10:22:24 -0700 Subject: [PATCH 12/49] TLS Ready --- .gitignore | 1 + LDAPCnx.cc | 91 ++++++++++++++--------------------------- LDAPCnx.h | 34 ++++++++------- README.md | 1 - index.js | 100 ++++++++++++++++++++------------------------- test/index.js | 49 ++++++++++++++++++++-- test/leakcheck | 7 +++- test/run_server.sh | 2 +- test/tls.js | 4 +- 9 files changed, 146 insertions(+), 143 deletions(-) diff --git a/.gitignore b/.gitignore index d9870ba..7eaa228 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ test/slapd.pid test/reconnect.js .DS_Store .fuse_* +*.log diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 6d66189..87264d0 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -30,14 +30,11 @@ void LDAPCnx::Init(Local exports) { Nan::SetPrototypeMethod(tpl, "add", Add); Nan::SetPrototypeMethod(tpl, "modify", Modify); Nan::SetPrototypeMethod(tpl, "rename", Rename); - Nan::SetPrototypeMethod(tpl, "initialize", Initialize); Nan::SetPrototypeMethod(tpl, "abandon", Abandon); Nan::SetPrototypeMethod(tpl, "errorstring", GetErr); Nan::SetPrototypeMethod(tpl, "close", Close); - Nan::SetPrototypeMethod(tpl, "errorno", GetErrNo); + Nan::SetPrototypeMethod(tpl, "errno", GetErrNo); Nan::SetPrototypeMethod(tpl, "fd", GetFD); - Nan::SetPrototypeMethod(tpl, "starttls", StartTLS); - Nan::SetPrototypeMethod(tpl, "installtls", InstallTLS); Nan::SetPrototypeMethod(tpl, "checktls", CheckTLS); constructor.Reset(tpl->GetFunction()); @@ -54,7 +51,34 @@ void LDAPCnx::New(const Nan::FunctionCallbackInfo& info) { ld->reconnect_callback = new Nan::Callback(info[1].As()); ld->disconnect_callback = new Nan::Callback(info[2].As()); ld->handle = NULL; - + + Nan::Utf8String url(info[3]); + int ver = LDAP_VERSION3; + int timeout = info[4]->NumberValue(); + int debug = info[5]->NumberValue(); + int verifycert = info[6]->NumberValue(); + int zero = 0; + + ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); + ld->ldap_callback->lc_add = OnConnect; + ld->ldap_callback->lc_del = OnDisconnect; + ld->ldap_callback->lc_arg = ld; + + if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { + Nan::ThrowError("Error init"); + return; + } + + struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; + + ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); + ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug); + ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); + ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); + ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &verifycert); + ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &zero); + info.GetReturnValue().Set(info.Holder()); return; } @@ -193,34 +217,6 @@ void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, lc->disconnect_callback->Call(0, NULL); } -void LDAPCnx::Initialize(const Nan::FunctionCallbackInfo& info) { - LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - Nan::Utf8String url(info[0]); - int ver = LDAP_VERSION3; - int timeout = info[1]->NumberValue(); - int debug = info[2]->NumberValue(); - - ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); - ld->ldap_callback->lc_add = OnConnect; - ld->ldap_callback->lc_del = OnDisconnect; - ld->ldap_callback->lc_arg = ld; - - if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { - Nan::ThrowError("Error init"); - return; - } - - struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; - - ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); - ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug); - ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); - ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); - ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); - - info.GetReturnValue().Set(info.This()); -} - void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); int err; @@ -234,33 +230,6 @@ void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().Set(ldap_unbind(ld->ld)); } -void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { - LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - int msgid; - int verifycert = info[0]->NumberValue(); - int val; - int res; - - if (verifycert == 0) { - val = LDAP_OPT_X_TLS_ALLOW; - } else { - val = LDAP_OPT_X_TLS_HARD; - } - ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &val); - val = 0; - ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &val); - - res = ldap_start_tls(ld->ld, NULL, NULL, &msgid); - - info.GetReturnValue().Set(msgid); -} - -void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { - LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - - info.GetReturnValue().Set(ldap_install_tls(ld->ld)); -} - void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); @@ -270,7 +239,7 @@ void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { void LDAPCnx::Abandon(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - info.GetReturnValue().Set(ldap_abandon(ld->ld, info[0]->NumberValue())); //findme + info.GetReturnValue().Set(ldap_abandon(ld->ld, info[0]->NumberValue())); } void LDAPCnx::GetErrNo(const Nan::FunctionCallbackInfo& info) { diff --git a/LDAPCnx.h b/LDAPCnx.h index cd297ab..e88eeac 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -15,25 +15,23 @@ class LDAPCnx : public Nan::ObjectWrap { explicit LDAPCnx(); ~LDAPCnx(); - static void New(const Nan::FunctionCallbackInfo& info); - static void Initialize(const Nan::FunctionCallbackInfo& info); - static void Event(uv_poll_t* handle, int status, int events); - static int OnConnect(LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, struct sockaddr *addr, struct ldap_conncb *ctx); + static void New (const Nan::FunctionCallbackInfo& info); + static void Event (uv_poll_t* handle, int status, int events); + static int OnConnect (LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, + struct sockaddr *addr, struct ldap_conncb *ctx); static void OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx); - static void Search(const Nan::FunctionCallbackInfo& info); - static void Delete(const Nan::FunctionCallbackInfo& info); - static void Bind(const Nan::FunctionCallbackInfo& info); - static void Add(const Nan::FunctionCallbackInfo& info); - static void Modify(const Nan::FunctionCallbackInfo& info); - static void Rename(const Nan::FunctionCallbackInfo& info); - static void Abandon(const Nan::FunctionCallbackInfo& info); - static void GetErr(const Nan::FunctionCallbackInfo& info); - static void Close(const Nan::FunctionCallbackInfo& info); - static void GetErrNo(const Nan::FunctionCallbackInfo& info); - static void GetFD(const Nan::FunctionCallbackInfo& info); - static void StartTLS(const Nan::FunctionCallbackInfo& info); - static void InstallTLS(const Nan::FunctionCallbackInfo& info); - static void CheckTLS(const Nan::FunctionCallbackInfo& info); + static void Search (const Nan::FunctionCallbackInfo& info); + static void Delete (const Nan::FunctionCallbackInfo& info); + static void Bind (const Nan::FunctionCallbackInfo& info); + static void Add (const Nan::FunctionCallbackInfo& info); + static void Modify (const Nan::FunctionCallbackInfo& info); + static void Rename (const Nan::FunctionCallbackInfo& info); + static void Abandon (const Nan::FunctionCallbackInfo& info); + static void GetErr (const Nan::FunctionCallbackInfo& info); + static void Close (const Nan::FunctionCallbackInfo& info); + static void GetErrNo (const Nan::FunctionCallbackInfo& info); + static void GetFD (const Nan::FunctionCallbackInfo& info); + static void CheckTLS (const Nan::FunctionCallbackInfo& info); ldap_conncb * ldap_callback; uv_poll_t * handle; diff --git a/README.md b/README.md index 0d56557..2e336bc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ This is an API-breaking release, but it should be easy to convert to the new API NOTE: The module has been renamed to `ldap-client` as `npm` no longer accepts capital letters. - Contributing ------------ diff --git a/index.js b/index.js index 7900003..ccc0129 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var binding = require('bindings')('LDAPCnx'); var LDAPError = require('./LDAPError'); +var assert = require('assert'); var _ = require('lodash'); function arg(val, def) { @@ -31,10 +32,9 @@ function Stats() { } function LDAP(opt, fn) { - this.callbacks = {}; + this.queue = {}; this.stats = new Stats(); - this.initialconnect = true; - + this.options = _.assign({ base: 'dc=com', filter: '(objectClass=*)', @@ -43,60 +43,33 @@ function LDAP(opt, fn) { ntimeout: 1000, timeout: 2000, debug: 0, - starttls: false, validatecert: true, connect: function() {}, disconnect: function() {} }, opt); - this.readyfn = fn; - - if (typeof opt.uri !== 'string') { - throw new LDAPError('Missing argument'); + if (typeof this.options.uri === 'string') { + this.options.uri = [ this.options.uri ]; } - - this.ld = new binding.LDAPCnx(this.onresult.bind(this), + + this.ld = new binding.LDAPCnx(this.dequeue.bind(this), this.onconnect.bind(this), - this.ondisconnect.bind(this)); - try { - this.ld.initialize(this.options.uri, this.options.ntimeout, this.options.debug); - } catch (e) { - //TODO: does init still need to throw? + this.ondisconnect.bind(this), + this.options.uri.join(' '), + this.options.ntimeout, + this.options.debug, + this.options.validatecert); + + if (typeof fn !== 'function') { + fn = function() {}; } - if (this.options.starttls) { - this.enqueue(this.ld.starttls(this.options.validatecert), function tlsStarted(err) { - if (err) return this.readyfn(err); - if ((err = this.ld.installtls()) !== 0) return this.readyfn(new LDAPError(this.ld.errorstring())); - if ((err = this.ld.checktls()) !== 1) return this.readyfn(new LDAPError('Expected TLS')); - return this.enqueue(this.ld.bind(undefined, undefined), this.do_ready.bind(this)); - }.bind(this)); - } else { - this.enqueue(this.ld.bind(undefined, undefined), this.do_ready.bind(this)); - } - - return this; + + return this.enqueue(this.ld.bind(undefined, undefined), fn); } -LDAP.prototype.do_ready = function(err) { - this.initialconnect = false; - if (typeof this.readyfn === 'function') this.readyfn(err); -}; - -LDAP.prototype.onresult = function(err, msgid, data) { - this.stats.results++; - if (this.callbacks[msgid]) { - clearTimeout(this.callbacks[msgid].timer); - this.callbacks[msgid](err, data); - delete this.callbacks[msgid]; - } else { - this.stats.lateresponses++; - } -}; - LDAP.prototype.onconnect = function() { this.stats.reconnects++; - if (this.initialconnect) return; // suppress initial connect event - this.options.connect(); + return this.options.connect(); }; LDAP.prototype.ondisconnect = function() { @@ -201,6 +174,17 @@ LDAP.prototype.close = function() { this.ld = undefined; }; +LDAP.prototype.dequeue = function(err, msgid, data) { + 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++; + } +}; + LDAP.prototype.enqueue = function(msgid, fn) { if (msgid == -1 || this.ld === undefined) { if (this.ld.errorstring() === 'Can\'t contact LDAP server') { @@ -211,9 +195,9 @@ LDAP.prototype.enqueue = function(msgid, fn) { // 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.callbacks).forEach(function fireTimeout(msgid) { - this.callbacks[msgid](new LDAPError('Timeout')); - delete this.callbacks[msgid]; + Object.keys(this.queue).forEach(function fireTimeout(msgid) { + this.queue[msgid](new LDAPError('Timeout')); + delete this.queue[msgid]; this.ld.abandon(msgid); }.bind(this)); } @@ -225,19 +209,25 @@ LDAP.prototype.enqueue = function(msgid, fn) { } fn.timer = setTimeout(function searchTimeout() { this.ld.abandon(msgid); - delete this.callbacks[msgid]; + delete this.queue[msgid]; fn(new LDAPError('Timeout')); this.stats.timeouts++; }.bind(this), this.options.timeout); - this.callbacks[msgid] = fn; + this.queue[msgid] = fn; this.stats.requests++; return this; }; -LDAP.BASE = 0; -LDAP.ONELEVEL = 1; -LDAP.SUBTREE = 2; -LDAP.SUBORDINATE = 3; -LDAP.DEFAULT = 4; +LDAP.BASE = 0; +LDAP.ONELEVEL = 1; +LDAP.SUBTREE = 2; +LDAP.SUBORDINATE = 3; +LDAP.DEFAULT = 4; + +LDAP.LDAP_OPT_X_TLS_NEVER = 0; +LDAP.LDAP_OPT_X_TLS_HARD = 1; +LDAP.LDAP_OPT_X_TLS_DEMAND = 2; +LDAP.LDAP_OPT_X_TLS_ALLOW = 3; +LDAP.LDAP_OPT_X_TLS_TRY = 4; module.exports = LDAP; diff --git a/test/index.js b/test/index.js index f76bdc3..f7b07cf 100644 --- a/test/index.js +++ b/test/index.js @@ -20,8 +20,8 @@ function showImage(what) { } -describe('LDAP', function(done) { - it ('Should initialize OK', function() { +describe('LDAP', function() { + it ('Should initialize OK', function(done) { ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', @@ -283,12 +283,53 @@ describe('LDAP', function(done) { it ('Should close and disconnect', function() { ldap.close(); }); - it ('Should connect again OK', function() { + it ('Should connect again OK', function(done) { ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*' }, done); }); - + it ('Should close again', function(done) { + ldap = new LDAP({ + uri: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*' + }, done); + }); + it ('Should connect over domain socket', function(done) { + ldap = new LDAP({ + uri: 'ldapi://%2ftmp%2fslapd.sock', + base: 'dc=sample,dc=com', + attrs: '*' + }, done); + }); + it ('Should search over domain socket', function(done) { + ldap.search({ + filter: '(cn=babs)', + scope: LDAP.SUBTREE + }, function(err, res) { + assert.ifError(err); + assert.equal(res.length, 1); + assert.equal(res[0].sn[0], 'Jensen'); + assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); + done(); + }); + }); + it ('Should survive a slight beating', function(done) { + this.timeout(5000); + var count = 0; + for (var x = 0 ; x < 1000 ; x++) { + ldap.search({ + base: 'dc=sample,dc=com', + filter: '(cn=albert)', + attrs: '*' + }, function(err, res) { + count++; + if (count >= 1000) { + done(); + } + }); + } + }); }); diff --git a/test/leakcheck b/test/leakcheck index 4f270ac..ffa2b4a 100644 --- a/test/leakcheck +++ b/test/leakcheck @@ -7,7 +7,11 @@ var assert = require('assert'); var ldap; var errors = {};; -ldap = new LDAP({uri: 'ldap://localhost:1234', connecttimeout: 1000}); +ldap = new LDAP({ + uri: 'ldaps://localhost:1235', + starttls: false, + verifycert: false +}); setInterval(function() { ldap.search({ base: 'dc=sample,dc=com', @@ -19,6 +23,7 @@ setInterval(function() { errors[err.message] = 0; } errors[err.message]++; + // assert(ldap.tlsactive()); return; } }); diff --git a/test/run_server.sh b/test/run_server.sh index 54aec72..8cb6667 100755 --- a/test/run_server.sh +++ b/test/run_server.sh @@ -10,7 +10,7 @@ $RM -rf openldap-data $MKDIR openldap-data $SLAPADD -f slapd.conf < startup.ldif -$SLAPD -d999 -f slapd.conf -hldap://localhost:1234 +$SLAPD -d999 -f slapd.conf -h "ldap://localhost:1234 ldapi://%2ftmp%2fslapd.sock ldaps://localhost:1235" SLAPD_PID=$! # slapd should be running now diff --git a/test/tls.js b/test/tls.js index 4b30eb7..91c9d47 100644 --- a/test/tls.js +++ b/test/tls.js @@ -10,7 +10,7 @@ var ldap; describe('LDAP TLS', function() { it ('Should fail TLS on cert validation', function(done) { ldap = new LDAP({ - uri: 'ldap://localhost:1234', + uri: 'ldaps://localhost:1235', base: 'dc=sample,dc=com', attrs: '*', starttls: true @@ -22,7 +22,7 @@ describe('LDAP TLS', function() { it ('Should connect', function(done) { this.timeout(10000); ldap = new LDAP({ - uri: 'ldap://localhost:1234', + uri: 'ldaps://localhost:1235', base: 'dc=sample,dc=com', attrs: '*', starttls: true, From a2e45ca08270eb2e5c7c3251e65dfd2994ce9d5d Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 10:25:36 -0700 Subject: [PATCH 13/49] Additional negative TLS test --- test/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/index.js b/test/index.js index f7b07cf..a3ec256 100644 --- a/test/index.js +++ b/test/index.js @@ -19,7 +19,6 @@ function showImage(what) { } - describe('LDAP', function() { it ('Should initialize OK', function(done) { ldap = new LDAP({ @@ -47,7 +46,10 @@ describe('LDAP', function() { ldap.timeout=1000; done(); }); - }); */ + }); */ + it ('Should show TLS not active', function() { + assert(ldap.tlsactive() === 0); + }); it ('Should return specified attrs', function(done) { ldap.search({ base: 'dc=sample,dc=com', From 2fefd24090e8d6b8622983f58607b22f5ef4d91a Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:31:32 -0700 Subject: [PATCH 14/49] Added full binary handling --- LDAPCnx.cc | 28 +++++++++++++++++++++++++++- LDAPCnx.h | 2 ++ test/index.js | 12 ++++++++++++ test/slapd.conf | 12 ++++++------ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 87264d0..aebc8b8 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -136,7 +136,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { js_result->Set(Nan::New(attrname).ToLocalChecked(), js_attr_vals); // TODO: check for binary settings - int bin = !strcmp(attrname, "jpegPhoto"); + int bin = isBinary(attrname); for (int i = 0 ; i < num_vals && vals[i] ; i++) { if (bin) { @@ -400,3 +400,29 @@ void LDAPCnx::Add(const Nan::FunctionCallbackInfo& info) { ldap_mods_free(ldapmods, 1); } + +// Attributes matching this list will be returned as Buffer()s + +int LDAPCnx::isBinary(char * attrname) { + if (!strcmp(attrname, "jpegPhoto") || + !strcmp(attrname, "photo") || + !strcmp(attrname, "personalSignature") || + !strcmp(attrname, "userCertificate") || + !strcmp(attrname, "cACertificate") || + !strcmp(attrname, "authorityRevocationList") || + !strcmp(attrname, "certificateRevocationList") || + !strcmp(attrname, "deltaRevocationList") || + !strcmp(attrname, "crossCertificatePair") || + !strcmp(attrname, "x500UniqueIdentifier") || + !strcmp(attrname, "audio") || + !strcmp(attrname, "javaSerializedObject") || + !strcmp(attrname, "thumbnailPhoto") || + !strcmp(attrname, "thumbnailLogo") || + !strcmp(attrname, "supportedAlgorithms") || + !strcmp(attrname, "protocolInformation") || + !strcmp(attrname, "objectGUID") || + strstr(attrname, ";binary")) { + return 1; + } + return 0; +} diff --git a/LDAPCnx.h b/LDAPCnx.h index e88eeac..41b965e 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -32,6 +32,7 @@ class LDAPCnx : public Nan::ObjectWrap { static void GetErrNo (const Nan::FunctionCallbackInfo& info); static void GetFD (const Nan::FunctionCallbackInfo& info); static void CheckTLS (const Nan::FunctionCallbackInfo& info); + static int isBinary (char * attrname); ldap_conncb * ldap_callback; uv_poll_t * handle; @@ -41,3 +42,4 @@ class LDAPCnx : public Nan::ObjectWrap { }; #endif + diff --git a/test/index.js b/test/index.js index a3ec256..3f37bc7 100644 --- a/test/index.js +++ b/test/index.js @@ -125,6 +125,18 @@ describe('LDAP', function() { done(); }); }); + it ('Should show the rootDSE', function(done) { + ldap.search({ + base: '', + scope: LDAP.BASE, + filter: '(objectClass=*)', + attrs: '+' + }, function(err, data) { + assert.ifError(err); + assert(data[0].namingContexts[0] === 'dc=sample,dc=com'); + done(); + }); + }); it ('Should delete', function(done) { ldap.delete('cn=Albert,ou=Accounting,dc=sample,dc=com', function(err) { assert.ifError(err); diff --git a/test/slapd.conf b/test/slapd.conf index cb94959..dc854fd 100644 --- a/test/slapd.conf +++ b/test/slapd.conf @@ -45,12 +45,12 @@ TLSCertificateKeyFile certs/device.key # Allow authenticated users read access # Allow anonymous users to authenticate # Directives needed to implement policy: -# access to dn.base="" by * read -# access to dn.base="cn=Subschema" by * read -# access to * -# by self write -# by users read -# by anonymous auth +access to dn.base="" by * read +access to dn.base="cn=Subschema" by * read +access to * + by self write + by users read + by anonymous read # # if no access controls are present, the default policy # allows anyone and everyone to read anything but restricts From e27d3ea771d8f06ad5b37abc30cac6df8d8a1b69 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:43:18 -0700 Subject: [PATCH 15/49] Doc updates --- README.md | 92 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 2e336bc..91d6fc0 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,24 @@ http://www.openldap.org installed. Now uses Nan to ensure it will build for all version of Node.js. -This is an API-breaking release, but it should be easy to convert to the new API. +***3.X is an API-breaking release***, but it should be easy to convert to the new API. NOTE: The module has been renamed to `ldap-client` as `npm` no longer accepts capital letters. Contributing ------------- +=== Any and all patches and pull requests are certainly welcome. Thanks to: ----------- +=== * Petr Běhan * YANG Xudong * Victor Powell +* Many other contributors Dependencies ------------- +=== Node >= 0.8 @@ -34,7 +35,7 @@ installed from http://www.openldap.org To install the latest release from npm: - npm install ldap-client + npm install --save ldap-client You will also require the LDAP Development Libraries (on Ubuntu, `sudo apt-get install libldap2-dev`) @@ -56,7 +57,6 @@ var LDAP = require('ldap-client'); var ldap = new LDAP({ uri: 'ldap://server', // string - starttls: false, // boolean, default is false validatecert: false, // Verify server certificate connecttimeout: -1, // seconds, default is -1 (infinite timeout), connect timeout base: 'dc=com', // default base for all future searches @@ -71,8 +71,7 @@ var ldap = new LDAP({ ``` -The reconnect handler is called on initial connect as well, so this function is a really good place -to do a bind() or any other things you want to set up for every connection. +The reconnect handler is called on initial connect as well, so this function is a really good place to do a bind() or any other things you want to set up for every connection. ```js var ldap = new LDAP({ @@ -88,24 +87,24 @@ var ldap = new LDAP({ } ``` -ldap.open() ------------ - -Deprecated. Currently, just calls the callback with no error. Feel free to omit. +TLS +=== +TLS can be used via the ldaps:// protocol string in the URI attribute on instantiation. If you want to eschew server certificate checking (if you have a self-signed cserver certificate, for example), you can add the `verifycert` attribute, which may contain one of the following values: ```js -ldap.open(function(err) { - if (err) { - // will never happen - } - // connection is ready. -}); +var LDAP=require('ldap-client'); + +LDAP.LDAP_OPT_X_TLS_NEVER = 0; +LDAP.LDAP_OPT_X_TLS_HARD = 1; +LDAP.LDAP_OPT_X_TLS_DEMAND = 2; +LDAP.LDAP_OPT_X_TLS_ALLOW = 3; +LDAP.LDAP_OPT_X_TLS_TRY = 4; ``` ldap.bind() ------------------ +=== Calling open automatically does an anonymous bind to check to make -sure the connection is actually open. If you call simplebind(), you +sure the connection is actually open. If you call `bind()`, you will upgrade the existing anonymous bind. ldap.bind(bind_options, function(err)); @@ -122,27 +121,33 @@ Aliased to `ldap.simplebind()` for backward compatibility. ldap.search() -------------- +=== ldap.search(search_options, function(err, data)); Options are provided as a JS object: ```js search_options = { - base: '', - scope: '', - filter: '', - attrs: '' // default is '*' + base: 'dc=com', + scope: LDAP.SUBTREE, + filter: '(objectClass=*)', + attrs: '*' } ``` +If one omits any of the above options, then sensible defaults will be used. One can also provide search defaults as part of instantiation. + Scopes are specified as one of the following integers: -* LDAP.BASE = 0; -* LDAP.ONELEVEL = 1; -* LDAP.SUBTREE = 2; -* LDAP.SUBORDINATE = 3; -* LDAP.DEFAULT = -1; +```js +var LDAP=require('ldap-client'); + +LDAP.BASE = 0; +LDAP.ONELEVEL = 1; +LDAP.SUBTREE = 2; +LDAP.SUBORDINATE = 3; +LDAP.DEFAULT = -1; +``` List of attributes you want is passed as simple string - join their names with space if you need more ('objectGUID sAMAccountName cname' is example of @@ -176,6 +181,10 @@ attributes are returned as Buffers too. There is currently no known way to do this for '\*' wildcard - patches are welcome (see discussion in issue #44 and pull #58 for some ideas). +Paged Search Results +=== +NB: Paged search results are not currently implemented. + LDAP servers are usually limited in how many items they are willing to return - 1024 or 4096 are some typical values. For larger LDAP directories, you need to either partition your results with filter, or use paged search. To get @@ -205,19 +214,23 @@ search_options = { } ``` +RootDSE +=== + As of version 1.2.0 you can also read the rootDSE entry of an ldap server. To do so, simply issue a read request with base set to an empty string: ```js search_options = { base: '', - scope: Connection.BASE, // 0 + scope: Connection.BASE, + attrs: '+' // ... other options as necessary } ``` ldap.findandbind() ------------------- +=== ldap.findandbind(fb_options, function(err, data)) @@ -284,7 +297,7 @@ is done my the LDAP server itself. ldap.add() ----------- +=== ldap.add(dn, [attrs], function(err)) @@ -300,7 +313,7 @@ var attrs = [ ``` ldap.modify() -------------- +=== ldap.modify(dn, [ changes ], function(err)) @@ -317,7 +330,7 @@ var changes = [ ``` ldap.rename() -------------- +=== ldap.rename(dn, newrdn, function(err)) @@ -330,7 +343,7 @@ ldap.rename('cn=name,dc=example,dc=com', 'cn=newname') ``` ldap.remove() -------------- +=== ldap.remove(dn, function(err)) @@ -347,15 +360,14 @@ ldap.remove('cn=name,dc=example,dc=com', function(err) { ``` Bugs ----- +=== Domain errors don't work properly. Domains are deprecated as of node 4, so I don't think I'm going to track it down. If you need domain handling, let me know. TODO Items ----------- +=== Basically, these are features I don't really need myself. * Referral chasing -* Binary attribute handling -* Paged search results +* Paged search results (create cookie.cc to store cookie bervals) From a491e6970821101339eaf6bb533852708d9d2784 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:45:16 -0700 Subject: [PATCH 16/49] Note-to-self on paged results. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91d6fc0..7205625 100644 --- a/README.md +++ b/README.md @@ -370,4 +370,8 @@ TODO Items Basically, these are features I don't really need myself. * Referral chasing -* Paged search results (create cookie.cc to store cookie bervals) +* Paged search results + +Notes on Paged Results +=== +To properly implement paged search results, we need to create another C++ class that represents the page cookie. This class should be instantiated to store the pointer to the ber cookie, and properly destroy itself when it goes out of scope. This object should be returned as part of the search results. \ No newline at end of file From 217413c799f0ba41651b9d74d1880f3f5cac8c3c Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:48:10 -0700 Subject: [PATCH 17/49] TLS notes updated. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7205625..6625239 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ var ldap = new LDAP({ TLS === -TLS can be used via the ldaps:// protocol string in the URI attribute on instantiation. If you want to eschew server certificate checking (if you have a self-signed cserver certificate, for example), you can add the `verifycert` attribute, which may contain one of the following values: +TLS can be used via the ldaps:// protocol string in the URI attribute on instantiation. If you want to eschew server certificate checking (if you have a self-signed cserver certificate, for example), you can set the `verifycert` attribute to `LDAP.LDAP_OPT_X_TLS_NEVER`, or one of the following values: ```js var LDAP=require('ldap-client'); From 98e49db679645f44a9016bc44a58642392ea6ce7 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:49:40 -0700 Subject: [PATCH 18/49] 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 516dd23..8be6797 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "2.0.7" + "version": "3.0.0" } From 715f17d51ccfb1ce7e85dbef19a60adf42e10670 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 11:59:40 -0700 Subject: [PATCH 19/49] Updates --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 6625239..d341629 100644 --- a/README.md +++ b/README.md @@ -175,12 +175,7 @@ Example of search result: Attributes themselves are usually returned as strings. There is a list of known binary attribute names hardcoded in C++ binding sources. Those are always -returned as Buffers, but the list is incomplete so far. You can take advantage -of RFC4522 and specify attribute names in the form '\;binary' - such -attributes are returned as Buffers too. There is currently no known way to do -this for '\*' wildcard - patches are welcome (see discussion in issue #44 and -pull #58 for some ideas). - +returned as Buffers, but the list is incomplete so far. Paged Search Results === NB: Paged search results are not currently implemented. From b10adfe8d592a8c352a8efc498d77fd5c486f560 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 5 Nov 2015 17:28:19 -0700 Subject: [PATCH 20/49] Notes on paged results --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d341629..9cd8233 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,10 @@ Basically, these are features I don't really need myself. * Referral chasing * Paged search results +* Filter escaping Notes on Paged Results === -To properly implement paged search results, we need to create another C++ class that represents the page cookie. This class should be instantiated to store the pointer to the ber cookie, and properly destroy itself when it goes out of scope. This object should be returned as part of the search results. \ No newline at end of file +To properly implement paged search results, we need to create another C++ class that represents the page cookie. This class should be instantiated to store the pointer to the ber cookie, and properly destroy itself when it goes out of scope. This object should be returned as part of the search results. + +[https://github.com/nodejs/node-addon-examples/blob/master/8_passing_wrapped/nan/myobject.cc](myobject.cc) seems to be a pretty good template. From 8e4da7dce6f7dfd3ca71e3a5a164dab1c51891a8 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Fri, 6 Nov 2015 09:14:27 -0700 Subject: [PATCH 21/49] Removed assert --- LDAPCnx.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index aebc8b8..b0936df 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -92,7 +92,6 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { LDAPMessage * entry = NULL; Local errparam; - assert(status == 0); int msgtype; switch(ldap_result(ld->ld, LDAP_RES_ANY, LDAP_MSG_ALL, &ldap_tv, &message)) { From d5c9d484342ac83bed1f7a86d8f660384fc724ab Mon Sep 17 00:00:00 2001 From: jeremyc Date: Mon, 9 Nov 2015 11:24:04 -0700 Subject: [PATCH 22/49] Issue #80 --- LDAPCnx.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index b0936df..89378e5 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -211,8 +211,9 @@ void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx) { // this fires when the connection closes LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; - - uv_poll_stop(lc->handle); + if (lc->handle) { + uv_poll_stop(lc->handle); + } lc->disconnect_callback->Call(0, NULL); } From 183d8da1c1f19b77616acd166981848941ef48c6 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Mon, 9 Nov 2015 11:47:57 -0700 Subject: [PATCH 23/49] Fix Issue #80 --- index.js | 2 +- test/index.js | 9 +++------ test/issues.js | 36 ++++++++++++++++++++++++++++++++++++ test/tls.js | 3 +-- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 test/issues.js diff --git a/index.js b/index.js index ccc0129..2acff5d 100644 --- a/index.js +++ b/index.js @@ -43,7 +43,7 @@ function LDAP(opt, fn) { ntimeout: 1000, timeout: 2000, debug: 0, - validatecert: true, + validatecert: LDAP.LDAP_OPT_X_TLS_HARD, connect: function() {}, disconnect: function() {} }, opt); diff --git a/test/index.js b/test/index.js index 3f37bc7..d3dbb4f 100644 --- a/test/index.js +++ b/test/index.js @@ -304,12 +304,8 @@ describe('LDAP', function() { attrs: '*' }, done); }); - it ('Should close again', function(done) { - ldap = new LDAP({ - uri: 'ldap://localhost:1234', - base: 'dc=sample,dc=com', - attrs: '*' - }, done); + it ('Should close again', function() { + ldap.close(); }); it ('Should connect over domain socket', function(done) { ldap = new LDAP({ @@ -341,6 +337,7 @@ describe('LDAP', function() { }, function(err, res) { count++; if (count >= 1000) { + ldap.close(); done(); } }); diff --git a/test/issues.js b/test/issues.js new file mode 100644 index 0000000..b06fc0e --- /dev/null +++ b/test/issues.js @@ -0,0 +1,36 @@ +/*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ + +'use strict'; + +var LDAP = require('../'); +var assert = require('assert'); +var fs = require('fs'); +var ldap; + +describe('Issues', function() { + it('Should fix Issue #80', function(done) { + var ldapConfig = { + schema: 'ldaps://', + host: 'localhost:1235', + binddn: 'cn=Babs,dc=sample,dc=com', + password: 'secret' + }; + var bind_options = { + binddn: ldapConfig.binddn, + password: ldapConfig.password + }; + + var uri = ldapConfig.schema + ldapConfig.host; + console.log(uri); + ldap = new LDAP({ + uri: uri, + validatecert: LDAP.LDAP_OPT_X_TLS_NEVER + }, function(err) { + ldap.bind(bind_options, function (err) { + assert.ifError(err); + ldap.close(); + done(); + }); + }); + }); +}); diff --git a/test/tls.js b/test/tls.js index 91c9d47..fb8aac1 100644 --- a/test/tls.js +++ b/test/tls.js @@ -9,11 +9,11 @@ var ldap; describe('LDAP TLS', function() { it ('Should fail TLS on cert validation', function(done) { + this.timeout(10000); ldap = new LDAP({ uri: 'ldaps://localhost:1235', base: 'dc=sample,dc=com', attrs: '*', - starttls: true }, function(err) { assert.ifError(!err); done(); @@ -25,7 +25,6 @@ describe('LDAP TLS', function() { uri: 'ldaps://localhost:1235', base: 'dc=sample,dc=com', attrs: '*', - starttls: true, validatecert: false }, function(err) { assert.ifError(err); From 9111d28a3411cda5759b13482ead83a35794a037 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Mon, 9 Nov 2015 11:48:19 -0700 Subject: [PATCH 24/49] 3.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8be6797..9236f11 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.0.0" + "version": "3.0.1" } From b94d0d3ae9f8794f957d083f5efb63ea1f667caf Mon Sep 17 00:00:00 2001 From: jeremyc Date: Mon, 9 Nov 2015 11:54:33 -0700 Subject: [PATCH 25/49] Added reconnect note. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9cd8233..93a659d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ If the connection fails during operation, the client library will handle the rec You must close() the instance to stop the reconnect behavior. +During long-running operation, you should be prepared to handle errors robustly - there is no telling when the underlying driver will be in the process of automatically reconnecting. `ldap.search()` and friends will happily return a `Timeout` or `Can't contact LDAP server` error if the server has temporarily gone away. So, though you **may** want to implement your app in the `new LDAP()` callback, it's perfectly acceptable (and maybe even recommended) to ignore the ready callback in `new LDAP()` and proceed anyway, knowing the library will eventually connect when it is able to. + API === From 290dc45c76c9b07b5c68ed6e5d94834999a8e30d Mon Sep 17 00:00:00 2001 From: jeremyc Date: Thu, 12 Nov 2015 10:49:22 -0700 Subject: [PATCH 26/49] Anon referals, better constants. --- LDAPCnx.cc | 17 +++++++++++++- LDAPCnx.h | 2 ++ index.js | 34 ++++++++++++++++----------- test/index.js | 56 ++++++++++++++++++++++++--------------------- test/issues.js | 57 +++++++++++++++++++++++++++++++++++----------- test/run_server.sh | 2 +- test/startup.ldif | 7 ++++++ 7 files changed, 121 insertions(+), 54 deletions(-) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 89378e5..3c157a7 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -57,6 +57,7 @@ void LDAPCnx::New(const Nan::FunctionCallbackInfo& info) { int timeout = info[4]->NumberValue(); int debug = info[5]->NumberValue(); int verifycert = info[6]->NumberValue(); + int referrals = info[7]->NumberValue(); int zero = 0; ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); @@ -74,11 +75,15 @@ void LDAPCnx::New(const Nan::FunctionCallbackInfo& info) { ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug); ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); - ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &verifycert); ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &zero); + ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, &referrals); + if (referrals) { + ldap_set_rebind_proc(ld->ld, OnRebind, ld); + } + info.GetReturnValue().Set(info.Holder()); return; } @@ -217,6 +222,14 @@ void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, lc->disconnect_callback->Call(0, NULL); } +int LDAPCnx::OnRebind(LDAP *ld, LDAP_CONST char *url, ber_tag_t request, + ber_int_t msgid, void *params) { + // this is a new *ld representing the new server connection + // so our existing code won't work! + + return LDAP_SUCCESS; +} + void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); int err; @@ -386,6 +399,7 @@ void LDAPCnx::Add(const Nan::FunctionCallbackInfo& info) { ldapmods[i]->mod_values = (char **) malloc(sizeof(char *) * (attrValsLength + 1)); for (int j = 0; j < attrValsLength; j++) { + // TODO: handle Buffers here. Nan::Utf8String modValue(attrValsHandle->Get(Nan::New(j))); ldapmods[i]->mod_values[j] = strdup(*modValue); } @@ -421,6 +435,7 @@ int LDAPCnx::isBinary(char * attrname) { !strcmp(attrname, "supportedAlgorithms") || !strcmp(attrname, "protocolInformation") || !strcmp(attrname, "objectGUID") || + !strcmp(attrname, "objectSid") || strstr(attrname, ";binary")) { return 1; } diff --git a/LDAPCnx.h b/LDAPCnx.h index 41b965e..47c0713 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -20,6 +20,8 @@ class LDAPCnx : public Nan::ObjectWrap { static int OnConnect (LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, struct sockaddr *addr, struct ldap_conncb *ctx); static void OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx); + static int OnRebind (LDAP *ld, LDAP_CONST char *url, ber_tag_t request, + ber_int_t msgid, void *params ); static void Search (const Nan::FunctionCallbackInfo& info); static void Delete (const Nan::FunctionCallbackInfo& info); static void Bind (const Nan::FunctionCallbackInfo& info); diff --git a/index.js b/index.js index 2acff5d..bb699d8 100644 --- a/index.js +++ b/index.js @@ -44,6 +44,7 @@ function LDAP(opt, fn) { timeout: 2000, debug: 0, validatecert: LDAP.LDAP_OPT_X_TLS_HARD, + referrals: 0, connect: function() {}, disconnect: function() {} }, opt); @@ -58,12 +59,13 @@ function LDAP(opt, fn) { this.options.uri.join(' '), this.options.ntimeout, this.options.debug, - this.options.validatecert); + this.options.validatecert, + this.options.referrals); if (typeof fn !== 'function') { fn = function() {}; } - + return this.enqueue(this.ld.bind(undefined, undefined), fn); } @@ -185,6 +187,8 @@ LDAP.prototype.dequeue = function(err, msgid, data) { } }; +LDAP.prototype.DEFAULT = 4; + LDAP.prototype.enqueue = function(msgid, fn) { if (msgid == -1 || this.ld === undefined) { if (this.ld.errorstring() === 'Can\'t contact LDAP server') { @@ -218,16 +222,20 @@ LDAP.prototype.enqueue = function(msgid, fn) { return this; }; -LDAP.BASE = 0; -LDAP.ONELEVEL = 1; -LDAP.SUBTREE = 2; -LDAP.SUBORDINATE = 3; -LDAP.DEFAULT = 4; - -LDAP.LDAP_OPT_X_TLS_NEVER = 0; -LDAP.LDAP_OPT_X_TLS_HARD = 1; -LDAP.LDAP_OPT_X_TLS_DEMAND = 2; -LDAP.LDAP_OPT_X_TLS_ALLOW = 3; -LDAP.LDAP_OPT_X_TLS_TRY = 4; +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; diff --git a/test/index.js b/test/index.js index d3dbb4f..b406eaf 100644 --- a/test/index.js +++ b/test/index.js @@ -40,12 +40,12 @@ describe('LDAP', function() { }); }); /* it ('Should timeout', function(done) { - ldap.timeout=1; // 1ms should do it - ldap.search('dc=sample,dc=com', '(cn=albert)', '*', function(err, msgid, res) { - // assert(err !== undefined); - ldap.timeout=1000; - done(); - }); + ldap.timeout=1; // 1ms should do it + ldap.search('dc=sample,dc=com', '(cn=albert)', '*', function(err, msgid, res) { + // assert(err !== undefined); + ldap.timeout=1000; + done(); + }); }); */ it ('Should show TLS not active', function() { assert(ldap.tlsactive() === 0); @@ -68,6 +68,7 @@ describe('LDAP', function() { ldap.search({ base: 'dc=sample,dc=com', filter: '(cn=wontfindthis)', + scope: LDAP.ONELEVEL, attrs: '*' }, function(err, res) { assert.equal(res.length, 0); @@ -270,30 +271,33 @@ describe('LDAP', function() { }); }); it ('Should accept unicode on modify', function(done) { - ldap.modify('cn=Albert,ou=Accounting,dc=sample,dc=com', [ - { op: 'replace', attr: 'title', vals: [ 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ' ] } - ], function(err) { - assert(!err, 'Bad unicode'); - ldap.search({ - base: 'dc=sample,dc=com', - filter: '(cn=albert)', - attrs: '*' - }, function(err, res) { - assert.equal(res[0].title[0], 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ'); - done(); - }); - }); - }); - it ('Should search with unicode', function(done) { - ldap.search({ - base: 'dc=sample,dc=com', - filter: '(title=ᓄᓇᕗᑦ ᒐᕙᒪᖓ)', - attrs: '*' + ldap.modify('cn=Albert,ou=Accounting,dc=sample,dc=com', [ + { op: 'replace', attr: 'title', vals: [ 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ' ] } + ], function(err) { + assert(!err, 'Bad unicode'); + ldap.search({ + base: 'dc=sample,dc=com', + filter: '(cn=albert)', + attrs: '*' }, function(err, res) { - assert.equal(res[0].dn, 'cn=Albert,ou=Accounting,dc=sample,dc=com'); + assert.equal(res[0].title[0], 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ'); done(); }); + }); }); + it ('Should search with weird inputs', function(done) { + ldap.search({ + base: 'dc=sample,dc=com', + scope: LDAP.ONELEVEL, + filter: '(objectClass=*)', + attrs: '+' + }, function(err, res) { + console.log(LDAP.BASE); + console.log(res.length); + assert.equal(res.length, 4); + done(); + }); + }); it ('Should close and disconnect', function() { ldap.close(); }); diff --git a/test/issues.js b/test/issues.js index b06fc0e..f537c64 100644 --- a/test/issues.js +++ b/test/issues.js @@ -11,25 +11,56 @@ describe('Issues', function() { it('Should fix Issue #80', function(done) { var ldapConfig = { schema: 'ldaps://', - host: 'localhost:1235', - binddn: 'cn=Babs,dc=sample,dc=com', - password: 'secret' + host: 'localhost:1235' }; - var bind_options = { - binddn: ldapConfig.binddn, - password: ldapConfig.password - }; - var uri = ldapConfig.schema + ldapConfig.host; - console.log(uri); + ldap = new LDAP({ uri: uri, validatecert: LDAP.LDAP_OPT_X_TLS_NEVER - }, function(err) { - ldap.bind(bind_options, function (err) { + }, function (err) { + assert.ifError(err); + done(); + }); + }); + it('Should search after Issue #80', function(done) { + ldap.search({ + base: 'dc=sample,dc=com', + filter: '(objectClass=*)' + }, function(err, res) { + assert.ifError(err); + assert.equal(res.length, 6); + done(); + }); + + }); + it('Base scope should work - Issue #81', function(done) { + assert.equal(ldap.DEFAULT, 4, 'ldap.DEFAULT const is not zero'); + assert.equal(LDAP.DEFAULT, 4, 'LDAP.DEFAULT const is not zero'); + assert.equal(LDAP.LDAP_OPT_X_TLS_TRY, 4); + ldap.search({ + base: 'dc=sample,dc=com', + scope: ldap.BASE, + filter: '(objectClass=*)' + }, function(err, res) { + + assert.equal(res.length, 1, 'Unexpected number of results'); + ldap.search({ + base: 'dc=sample,dc=com', + scope: LDAP.SUBTREE, + filter: '(objectClass=*)' + }, function(err, res) { assert.ifError(err); - ldap.close(); - done(); + assert.equal(res.length, 6, 'Unexpected number of results'); + ldap.search({ + base: 'dc=sample,dc=com', + scope: LDAP.ONELEVEL, + filter: '(objectClass=*)' + }, function(err, res) { + assert.ifError(err); + assert.equal(res.length, 4, 'Unexpected number of results'); + done(); + }); }); }); }); diff --git a/test/run_server.sh b/test/run_server.sh index 8cb6667..c4f61ea 100755 --- a/test/run_server.sh +++ b/test/run_server.sh @@ -10,7 +10,7 @@ $RM -rf openldap-data $MKDIR openldap-data $SLAPADD -f slapd.conf < startup.ldif -$SLAPD -d999 -f slapd.conf -h "ldap://localhost:1234 ldapi://%2ftmp%2fslapd.sock ldaps://localhost:1235" +$SLAPD -d999 -f slapd.conf -h "ldap://:1234 ldapi://%2ftmp%2fslapd.sock ldaps://localhost:1235" SLAPD_PID=$! # slapd should be running now diff --git a/test/startup.ldif b/test/startup.ldif index 5e6f2b3..de5cb73 100644 --- a/test/startup.ldif +++ b/test/startup.ldif @@ -278,3 +278,10 @@ objectClass: top cn: Manager sn: Root userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 + +#dn: dc=666,dc=sample,dc=com +#objectClass: dcObject +#objectClass: referral +#objectClass: top +#dc: 666 +#ref: ldap://yk-ldap0.ssimicro.com/dc=666,dc=ssi From dbaf781600840f825baeaa8bb5d37f0af7c1a824 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 10 Nov 2015 19:27:00 -0700 Subject: [PATCH 27/49] 3.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9236f11..799875e 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.0.1" + "version": "3.0.2" } From 7990bba0cc2f010d8f6357b654095ab21b169b24 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Mon, 16 Nov 2015 15:39:54 -0700 Subject: [PATCH 28/49] Added escaping functions --- README.md | 34 +++++++++++++++++++++++++++++++++- index.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 93a659d..84c2572 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,39 @@ ldap.remove('cn=name,dc=example,dc=com', function(err) { }); ``` +Escaping +=== +Yes, Virginia, there's such a thing as LDAP injection attacks. + +There are a few helper functions to ensure you are escaping your input properly. + +**escapefn(type, template)** +Returns a function that escapes the provided parameters and inserts them into the provided template: + +```js +var LDAP = require('ldap-client'); +var userSearch = LDAP.escapefn('filter', + '(&(objectClass=%s)(cn=%s))'); + +... +ldap.search({ + filter: userSearch('posixUser', username), + scope: LDAP.SUBTREE +}, function(err, data) { + ... +}); +``` +Since the escaping rules are different for DNs vs search filters, `type` should be one of `'filter'` or `'dn'`. + +To escape a single string, use one of `LDAP.stringEscapeDN` or `LDAP.stringEscapeFilter`: + +```js +var LDAP=require('ldap-client'); + +LDAP.stringEscapeDN('dc=foo,dc=bar;baz'); +// ==> 'dc=doo,dc=bar\;baz' +``` + Bugs === Domain errors don't work properly. Domains are deprecated as of node 4, @@ -366,7 +399,6 @@ TODO Items === Basically, these are features I don't really need myself. -* Referral chasing * Paged search results * Filter escaping diff --git a/index.js b/index.js index bb699d8..fdd5eed 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var binding = require('bindings')('LDAPCnx'); var LDAPError = require('./LDAPError'); var assert = require('assert'); +var util = require('util'); var _ = require('lodash'); function arg(val, def) { @@ -14,6 +15,35 @@ function arg(val, def) { return def; } +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; @@ -187,8 +217,6 @@ LDAP.prototype.dequeue = function(err, msgid, data) { } }; -LDAP.prototype.DEFAULT = 4; - LDAP.prototype.enqueue = function(msgid, fn) { if (msgid == -1 || this.ld === undefined) { if (this.ld.errorstring() === 'Can\'t contact LDAP server') { @@ -222,6 +250,26 @@ LDAP.prototype.enqueue = function(msgid, fn) { 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.stringEscapeDN = LDAP.escapefn('dn', '%s'); +LDAP.stringEscapeFilter = LDAP.escapefn('filter', '%s'); + function setConst(target, name, val) { target.prototype[name] = target[name] = val; } From 046b7cf407d426e60d2eab31c800fd643cef5550 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 17 Nov 2015 11:07:11 -0700 Subject: [PATCH 29/49] Removed stringEscapeDN due to danger --- README.md | 20 +++++++++++++++++--- index.js | 1 - 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 84c2572..f5cac69 100644 --- a/README.md +++ b/README.md @@ -380,13 +380,27 @@ ldap.search({ ``` Since the escaping rules are different for DNs vs search filters, `type` should be one of `'filter'` or `'dn'`. -To escape a single string, use one of `LDAP.stringEscapeDN` or `LDAP.stringEscapeFilter`: +To escape a single string, `LDAP.stringEscapeFilter`: ```js var LDAP=require('ldap-client'); +var user = "John O'Doe"; + +LDAP.stringEscapeFilter('(username=' + user + ')'); +// ==> '(username=John O\'Doe)' +``` + +Note there is no function for string escaping a DN - DN escaping has special rules for escaping the beginning and end of values in the DN, so the best way to safely escape DNs is to use the `escapefn` with a template: + +```js +var LDAP = require('ldap-client'); +var escapeDN = LDAP.escapefn('dn', + 'cn=%s,dc=sample,dc=com'); + +... +var safeDN = escapeDN(" O'Doe"); +// => "cn=\ O\'Doe,dc=sample,dc=com" -LDAP.stringEscapeDN('dc=foo,dc=bar;baz'); -// ==> 'dc=doo,dc=bar\;baz' ``` Bugs diff --git a/index.js b/index.js index fdd5eed..ed7db1e 100644 --- a/index.js +++ b/index.js @@ -267,7 +267,6 @@ LDAP.escapefn = function(type, template) { }; }; -LDAP.stringEscapeDN = LDAP.escapefn('dn', '%s'); LDAP.stringEscapeFilter = LDAP.escapefn('filter', '%s'); function setConst(target, name, val) { From 1d15fe6962cd2b87c12c855b1c053cb6e7892d98 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 17 Nov 2015 14:26:57 -0700 Subject: [PATCH 30/49] Escaping Tests --- test/escaping.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/escaping.js diff --git a/test/escaping.js b/test/escaping.js new file mode 100644 index 0000000..63085c7 --- /dev/null +++ b/test/escaping.js @@ -0,0 +1,40 @@ +/*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ + +'use strict'; + +var LDAP = require('../'); +var assert = require('assert'); +var ldap; + +var dn_esc = LDAP.escapefn('dn', '#dc=%s,dc=%s'); +var filter_esc =LDAP.escapefn('filter', '(objectClass=%s)'); + +describe('Escaping', function() { + it ('Should initialize OK', function() { + ldap = new LDAP({ + uri: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*' + }); + }); + it('Should escape a dn', function() { + assert.equal(dn_esc('#foo', 'bar;baz'), '#dc=#foo,dc=bar\\;baz'); + }); + it('Should escape a filter', function() { + assert.equal(filter_esc('StarCorp*'), '(objectClass=StarCorp\\2A)'); + }); + it('Should escape Parens', function() { + var esc = LDAP.escapefn('filter', '(cn=%s)'); + assert.equal(filter_esc('weird_but_legal_username_with_parens()'), + '(objectClass=weird_but_legal_username_with_parens\\28\\29)'); + }); + it('Should escape Parens', function() { + var esc = LDAP.escapefn('filter', '(cn=%s)'); + assert.equal(filter_esc('weird_but_legal_username_with_parens()'), + '(objectClass=weird_but_legal_username_with_parens\\28\\29)'); + }); + it('Should escape an obvious injection', function() { + var esc = LDAP.escapefn('filter', '(cn=%s)'); + assert.equal(esc('*)|(password=*)'), '(cn=\\2A\\29|\\28password=\\2A\\29)'); + }); +}); From 109491545d9c42105083952aed968ad58a8fa3c8 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 17 Nov 2015 14:27:34 -0700 Subject: [PATCH 31/49] 3.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 799875e..09d5049 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.0.2" + "version": "3.1.0" } From 9e66a30939a9ab8ba949a4d8e62462e4d84880ec Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 17 Nov 2015 14:31:44 -0700 Subject: [PATCH 32/49] Remove logging. --- test/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/index.js b/test/index.js index b406eaf..f993c41 100644 --- a/test/index.js +++ b/test/index.js @@ -292,8 +292,6 @@ describe('LDAP', function() { filter: '(objectClass=*)', attrs: '+' }, function(err, res) { - console.log(LDAP.BASE); - console.log(res.length); assert.equal(res.length, 4); done(); }); From 544c874d8720315e8914ba1560c646608c1b2235 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 17 Nov 2015 14:31:50 -0700 Subject: [PATCH 33/49] Fix link. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5cac69..df6d95f 100644 --- a/README.md +++ b/README.md @@ -420,4 +420,4 @@ Notes on Paged Results === To properly implement paged search results, we need to create another C++ class that represents the page cookie. This class should be instantiated to store the pointer to the ber cookie, and properly destroy itself when it goes out of scope. This object should be returned as part of the search results. -[https://github.com/nodejs/node-addon-examples/blob/master/8_passing_wrapped/nan/myobject.cc](myobject.cc) seems to be a pretty good template. +[myobject.cc](https://github.com/nodejs/node-addon-examples/blob/master/8_passing_wrapped/nan/myobject.cc) seems to be a pretty good template. From c9f727b46d3669331b532e19af57426b30e93e8c Mon Sep 17 00:00:00 2001 From: jeremyc Date: Tue, 1 Dec 2015 16:13:33 -0700 Subject: [PATCH 34/49] Re-added StartTLS as experimental. --- LDAPCnx.cc | 19 +++++++++++++- LDAPCnx.h | 2 ++ index.js | 8 ++++++ test/ldaps.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/tls.js | 28 +++++++++++++++----- 5 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 test/ldaps.js diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 3c157a7..532317f 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -35,6 +35,8 @@ void LDAPCnx::Init(Local exports) { Nan::SetPrototypeMethod(tpl, "close", Close); Nan::SetPrototypeMethod(tpl, "errno", GetErrNo); Nan::SetPrototypeMethod(tpl, "fd", GetFD); + Nan::SetPrototypeMethod(tpl, "installtls", InstallTLS); + Nan::SetPrototypeMethod(tpl, "starttls", StartTLS); Nan::SetPrototypeMethod(tpl, "checktls", CheckTLS); constructor.Reset(tpl->GetFunction()); @@ -113,7 +115,6 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { } else { errparam = Nan::Undefined(); } - switch ( msgtype = ldap_msgtype( message ) ) { case LDAP_RES_SEARCH_REFERENCE: break; @@ -243,6 +244,22 @@ void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().Set(ldap_unbind(ld->ld)); } +void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + int msgid; + int res; + + res = ldap_start_tls(ld->ld, NULL, NULL, &msgid); + + info.GetReturnValue().Set(msgid); +} + +void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + info.GetReturnValue().Set(ldap_install_tls(ld->ld)); +} + void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); diff --git a/LDAPCnx.h b/LDAPCnx.h index 47c0713..9f9c64b 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -33,6 +33,8 @@ class LDAPCnx : public Nan::ObjectWrap { static void Close (const Nan::FunctionCallbackInfo& info); static void GetErrNo (const Nan::FunctionCallbackInfo& info); static void GetFD (const Nan::FunctionCallbackInfo& info); + static void StartTLS (const Nan::FunctionCallbackInfo& info); + static void InstallTLS (const Nan::FunctionCallbackInfo& info); static void CheckTLS (const Nan::FunctionCallbackInfo& info); static int isBinary (char * attrname); diff --git a/index.js b/index.js index ed7db1e..f6eb488 100644 --- a/index.js +++ b/index.js @@ -109,6 +109,14 @@ LDAP.prototype.ondisconnect = function() { 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(); }; diff --git a/test/ldaps.js b/test/ldaps.js new file mode 100644 index 0000000..fb8aac1 --- /dev/null +++ b/test/ldaps.js @@ -0,0 +1,71 @@ +/*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ + +'use strict'; + +var LDAP = require('../'); +var assert = require('assert'); +var fs = require('fs'); +var ldap; + +describe('LDAP TLS', function() { + it ('Should fail TLS on cert validation', function(done) { + this.timeout(10000); + ldap = new LDAP({ + uri: 'ldaps://localhost:1235', + base: 'dc=sample,dc=com', + attrs: '*', + }, function(err) { + assert.ifError(!err); + done(); + }); + }); + it ('Should connect', function(done) { + this.timeout(10000); + ldap = new LDAP({ + uri: 'ldaps://localhost:1235', + base: 'dc=sample,dc=com', + attrs: '*', + validatecert: false + }, function(err) { + assert.ifError(err); + done(); + }); + }); + it ('Should search via TLS', function(done) { + ldap.search({ + filter: '(cn=babs)', + scope: LDAP.SUBTREE + }, function(err, res) { + assert.ifError(err); + assert.equal(res.length, 1); + assert.equal(res[0].sn[0], 'Jensen'); + assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); + done(); + }); + }); + it ('Should findandbind()', function(done) { + ldap.findandbind({ + base: 'dc=sample,dc=com', + filter: '(cn=Charlie)', + attrs: '*', + password: 'foobarbaz' + }, function(err, data) { + assert.ifError(err); + done(); + }); + }); + it ('Should fail findandbind()', function(done) { + ldap.findandbind({ + base: 'dc=sample,dc=com', + filter: '(cn=Charlie)', + attrs: 'cn', + password: 'foobarbax' + }, function(err, data) { + assert.ifError(!err); + done(); + }); + }); + it ('Should still have TLS', function() { + assert(ldap.tlsactive()); + }); +}); diff --git a/test/tls.js b/test/tls.js index fb8aac1..061f2ab 100644 --- a/test/tls.js +++ b/test/tls.js @@ -8,27 +8,40 @@ var fs = require('fs'); var ldap; describe('LDAP TLS', function() { + /* + this succeeds, but it shouldn't + starttls is beta - at best - right now... it ('Should fail TLS on cert validation', function(done) { this.timeout(10000); ldap = new LDAP({ - uri: 'ldaps://localhost:1235', + uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', - attrs: '*', + attrs: '*' }, function(err) { - assert.ifError(!err); - done(); + ldap.starttls(function(err) { + console.log('ERR', err); + assert.ifError(err); + ldap.installtls(); + assert(ldap.tlsactive() == 1); + done(); + }); }); - }); + }); */ it ('Should connect', function(done) { this.timeout(10000); ldap = new LDAP({ - uri: 'ldaps://localhost:1235', + uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*', validatecert: false }, function(err) { assert.ifError(err); - done(); + ldap.starttls(function(err) { + assert.ifError(err); + ldap.installtls(); + assert(ldap.tlsactive()); + done(); + }); }); }); it ('Should search via TLS', function(done) { @@ -67,5 +80,6 @@ describe('LDAP TLS', function() { }); it ('Should still have TLS', function() { assert(ldap.tlsactive()); + ldap.close(); }); }); From 70896ac694f1a79df28c57ab73fc1ebdb1837948 Mon Sep 17 00:00:00 2001 From: Thomas Cort Date: Wed, 2 Mar 2016 10:19:44 -0500 Subject: [PATCH 35/49] findandbind: pass found entry to callback Prior versions of `findandbind()` passed `(err, data)` to the callback. This functionality was broken, I believe inadvertently, in commit a4b9ce6e8eda61d1c86faf20995464d538b20f6c It's still documented in the README and the callbacks in the unit tests still accept `(err, data)`. Since it was broken in a minor version bump (3.0->3.1), it will break existing code that relies on `data` being passed back and uses the caret operator to specify dependency versions. Restore that functionality, by passing the found entry to the callback supplied to `findandbind()`. --- index.js | 8 ++++++-- test/index.js | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index f6eb488..4467379 100644 --- a/index.js +++ b/index.js @@ -193,10 +193,14 @@ LDAP.prototype.findandbind = function(opt, fn) { 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, fn); + 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, fn); + this.authbind(data[0].dn, opt.password, function authbindResult(err) { + fn(err, data[0]); + }); } return undefined; }.bind(this)); diff --git a/test/index.js b/test/index.js index f993c41..c9756f1 100644 --- a/test/index.js +++ b/test/index.js @@ -89,6 +89,7 @@ describe('LDAP', function() { password: 'foobarbaz' }, function(err, data) { assert.ifError(err); + assert.equal(data.cn, 'Charlie'); done(); }); }); @@ -100,6 +101,7 @@ describe('LDAP', function() { password: 'foobarbaz' }, function(err, data) { assert.ifError(err); + assert.equal(data.cn, 'Charlie'); done(); }); }); From 1ce1cbd545f45ba973497b740bbfdc4b11f15152 Mon Sep 17 00:00:00 2001 From: Thomas Cort Date: Wed, 2 Mar 2016 13:43:45 -0500 Subject: [PATCH 36/49] README.md: fix name of onconnection function in docs The function that gets called when a connection is established comes from the `connect` option (see [here](https://github.com/jeremycx/node-LDAP/blob/master/index.js#L78) and [here](https://github.com/jeremycx/node-LDAP/blob/master/index.js#L104)). It was documented as the `reconnect` option. Fix the documentation to match the code. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df6d95f..2914ee6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You will also require the LDAP Development Libraries (on Ubuntu, `sudo apt-get i Reconnection ========== -If the connection fails during operation, the client library will handle the reconnection, calling the function specified in the reconnect option. This callback is a good place to put bind()s and other things you want to always be in place. +If the connection fails during operation, the client library will handle the reconnection, calling the function specified in the connect option. This callback is a good place to put bind()s and other things you want to always be in place. You must close() the instance to stop the reconnect behavior. @@ -65,7 +65,7 @@ var ldap = new LDAP({ attrs: '*', // default attribute list for future searches filter: '(objectClass=*)', // default filter for all future searches scope: LDAP.SUBTREE, // default scope for all future searches - reconnect: function(), // optional function to call when connect/reconnect occurs + connect: function(), // optional function to call when connect/reconnect occurs disconnect: function(), // optional function to call when disconnect occurs }, function(err) { // connected and ready @@ -73,12 +73,12 @@ var ldap = new LDAP({ ``` -The reconnect handler is called on initial connect as well, so this function is a really good place to do a bind() or any other things you want to set up for every connection. +The connect handler is called on initial connect as well as on reconnect, so this function is a really good place to do a bind() or any other things you want to set up for every connection. ```js var ldap = new LDAP({ uri: 'ldap://server', - reconnect: function() { + connect: function() { ldap.bind({ binddn: 'cn=admin,dc=com', password: 'supersecret' From 27670d98946f5fb55ea13ec9a8653a3a1e1b713a Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 2 Mar 2016 14:57:41 -0700 Subject: [PATCH 37/49] Connect conext now correct. --- index.js | 2 +- test/issues.js | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 4467379..f93f533 100644 --- a/index.js +++ b/index.js @@ -101,7 +101,7 @@ function LDAP(opt, fn) { LDAP.prototype.onconnect = function() { this.stats.reconnects++; - return this.options.connect(); + return this.options.connect.call(this); }; LDAP.prototype.ondisconnect = function() { diff --git a/test/issues.js b/test/issues.js index f537c64..6b569f9 100644 --- a/test/issues.js +++ b/test/issues.js @@ -7,14 +7,15 @@ var assert = require('assert'); var fs = require('fs'); var ldap; +var ldapConfig = { + schema: 'ldaps://', + host: 'localhost:1235' +}; +var uri = ldapConfig.schema + ldapConfig.host; + + describe('Issues', function() { it('Should fix Issue #80', function(done) { - var ldapConfig = { - schema: 'ldaps://', - host: 'localhost:1235' - }; - var uri = ldapConfig.schema + ldapConfig.host; - ldap = new LDAP({ uri: uri, validatecert: LDAP.LDAP_OPT_X_TLS_NEVER @@ -34,6 +35,21 @@ describe('Issues', function() { }); }); + it('Connect context should be ldap object - Issue #84', function(done) { + ldap = new LDAP({ + uri: uri, + validatecert: LDAP.LDAP_OPT_X_TLS_NEVER, + connect: function() { + assert(typeof this.bind === 'function'); + ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'secret'}, function(err) { + assert.ifError(err); + done(); + }); + } + }, function (err) { + assert.ifError(err); + }); + }); it('Base scope should work - Issue #81', function(done) { assert.equal(ldap.DEFAULT, 4, 'ldap.DEFAULT const is not zero'); assert.equal(LDAP.DEFAULT, 4, 'LDAP.DEFAULT const is not zero'); From ede0780f875517436e8a53d75d4657594b3527ad Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 2 Mar 2016 14:58:52 -0700 Subject: [PATCH 38/49] 3.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09d5049..aa124e9 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.1.0" + "version": "3.1.1" } From a05a5314efb3dc1d09b758b9da49b73ad0cf50c8 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 2 Mar 2016 14:58:57 -0700 Subject: [PATCH 39/49] 3.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa124e9..c4f35e6 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.1.1" + "version": "3.1.2" } From d028bbfdc64c266e4adfbf6d101feb0cc31ab793 Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 2 Mar 2016 15:03:47 -0700 Subject: [PATCH 40/49] Update node-gyp --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4f35e6..25084aa 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "bindings": "^1.2.1", "lodash": "^3.10.1", "nan": "^2.0.5", - "node-gyp": "^1.0.3" + "node-gyp": "" }, "engines": { "node": ">= 0.8.0" From 49bf1930378063e295ba799bf2a729c74ba4211c Mon Sep 17 00:00:00 2001 From: jeremyc Date: Wed, 2 Mar 2016 15:07:10 -0700 Subject: [PATCH 41/49] 3.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25084aa..ccf3720 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,5 @@ "engines": { "node": ">= 0.8.0" }, - "version": "3.1.2" + "version": "3.1.3" } From 0deec55b1068fe7087e59eb13be41e3b83557437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20B=C4=9Bhan?= Date: Tue, 22 Nov 2016 00:39:25 +0100 Subject: [PATCH 42/49] restore support for paging --- LDAP.cc | 2 ++ LDAPCnx.cc | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- LDAPCookie.cc | 42 ++++++++++++++++++++++++++++++++++++++++++ LDAPCookie.h | 25 +++++++++++++++++++++++++ binding.gyp | 2 +- index.js | 8 +++++++- 6 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 LDAPCookie.cc create mode 100644 LDAPCookie.h diff --git a/LDAP.cc b/LDAP.cc index c357da3..7ffa84e 100644 --- a/LDAP.cc +++ b/LDAP.cc @@ -1,8 +1,10 @@ #include #include "LDAPCnx.h" +#include "LDAPCookie.h" void InitAll(v8::Local exports) { LDAPCnx::Init(exports); + LDAPCookie::Init(exports); } NODE_MODULE(LDAPCnx, InitAll) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 532317f..0bf9252 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -1,4 +1,5 @@ #include "LDAPCnx.h" +#include "LDAPCookie.h" static struct timeval ldap_tv = { 0, 0 }; @@ -157,11 +158,38 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { ber_free(berptr,0); ldap_memfree(dn); } // all entries done. + + Local result_container = Nan::New(); + result_container->Set(Nan::New("data").ToLocalChecked(), js_result_list); + + LDAPControl** serverCtrls; + ldap_parse_result(ld->ld, message, + NULL, // int* errcodep + NULL, // char** matcheddnp + NULL, // char** errmsp + NULL, // char*** referralsp + &serverCtrls, + 0 // freeit + ); + if (serverCtrls) { + struct berval* cookie = NULL; + ldap_parse_page_control(ld->ld, serverCtrls, NULL, &cookie); + if (!cookie || cookie->bv_val == NULL || !*cookie->bv_val) { + if (cookie) + ber_bvfree(cookie); + } else { + Local cookieWrap = LDAPCookie::NewInstance(); + LDAPCookie* cookieContainer = ObjectWrap::Unwrap(cookieWrap); + cookieContainer->SetCookie(cookie); + result_container->Set(Nan::New("cookie").ToLocalChecked(), cookieWrap); + } + ldap_controls_free(serverCtrls); + } Local argv[] = { errparam, Nan::New(ldap_msgid(message)), - js_result_list + result_container }; ld->callback->Call(3, argv); break; @@ -320,6 +348,8 @@ void LDAPCnx::Search(const Nan::FunctionCallbackInfo& info) { Nan::Utf8String filter(info[1]); Nan::Utf8String attrs(info[2]); int scope = info[3]->NumberValue(); + int pagesize = info[4]->NumberValue();; + LDAPCookie* cookie = NULL; int msgid = 0; char * attrlist[255]; @@ -332,8 +362,24 @@ void LDAPCnx::Search(const Nan::FunctionCallbackInfo& info) { if (++ap >= &attrlist[255]) break; + LDAPControl* page_control[2]; + page_control[0] = NULL; + page_control[1] = NULL; + if (pagesize > 0) { + if (info[5]->IsObject() && !info[5]->ToObject().IsEmpty()) + cookie = Nan::ObjectWrap::Unwrap(info[5]->ToObject()); + if (cookie) { + ldap_create_page_control(ld->ld, pagesize, cookie->GetCookie(), 0, &page_control[0]); + } else { + ldap_create_page_control(ld->ld, pagesize, NULL, 0, &page_control[0]); + } + } + ldap_search_ext(ld->ld, *base, scope, *filter , (char **)attrlist, 0, - NULL, NULL, NULL, 0, &msgid); + page_control, NULL, NULL, 0, &msgid); + if (pagesize > 0) { + ldap_control_free(page_control[0]); + } free(bufhead); diff --git a/LDAPCookie.cc b/LDAPCookie.cc new file mode 100644 index 0000000..32f823e --- /dev/null +++ b/LDAPCookie.cc @@ -0,0 +1,42 @@ +#include "LDAPCookie.h" + +#include + +Nan::Persistent LDAPCookie::constructor; + +void LDAPCookie::New(const Nan::FunctionCallbackInfo& info) { + LDAPCookie* obj = new LDAPCookie(); + obj->Wrap(info.This()); + + info.GetReturnValue().Set(info.This()); +} + +LDAPCookie::~LDAPCookie() { + if (val_) { + fprintf(stderr, "cookie cleanup"); + ber_bvfree(val_); + } +} + +void LDAPCookie::Init(v8::Local exports) { + Nan::HandleScope scope; + v8::Local tpl = Nan::New(New); + // legal? No idea, just an attempt to prevent polluting javascript global namespace with + // something that doesn't make sense to construct from JS side. Appears to work (doesn't + // crash, paging works). + // tpl->SetClassName(Nan::New("LDAPInternalCookie").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + constructor.Reset(tpl->GetFunction()); +} + +v8::Local LDAPCookie::NewInstance() { + Nan::EscapableHandleScope scope; + + const unsigned argc = 1; + v8::Local argv[argc] = { Nan::Undefined() }; + v8::Local cons = Nan::New(constructor); + v8::Local instance = cons->NewInstance(argc, argv); + + return scope.Escape(instance); +} + diff --git a/LDAPCookie.h b/LDAPCookie.h new file mode 100644 index 0000000..574af6d --- /dev/null +++ b/LDAPCookie.h @@ -0,0 +1,25 @@ +#ifndef LDAPCOOKIE_H +#define LDAPCOOKIE_H + +#include + +class LDAPCookie : public Nan::ObjectWrap { + public: + static void Init(v8::Local exports); + static v8::Local NewInstance(); + + void SetCookie(struct berval* cookie) { val_ = cookie; } + struct berval* GetCookie() const { return val_; } + + private: + static Nan::Persistent constructor; + + LDAPCookie() {}; + ~LDAPCookie(); + + static void New(const Nan::FunctionCallbackInfo& info); + + struct berval* val_; +}; + +#endif diff --git a/binding.gyp b/binding.gyp index 04efb4d..5eab7ef 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,7 +2,7 @@ "targets": [ { "target_name": "LDAPCnx", - "sources": [ "LDAP.cc", "LDAPCnx.cc" ], + "sources": [ "LDAP.cc", "LDAPCnx.cc", "LDAPCookie.cc" ], "include_dirs" : [ " Date: Tue, 22 Nov 2016 00:50:51 +0100 Subject: [PATCH 43/49] update documentation --- README.md | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2914ee6..ae39500 100644 --- a/README.md +++ b/README.md @@ -178,36 +178,31 @@ Example of search result: Attributes themselves are usually returned as strings. There is a list of known binary attribute names hardcoded in C++ binding sources. Those are always returned as Buffers, but the list is incomplete so far. + Paged Search Results === -NB: Paged search results are not currently implemented. LDAP servers are usually limited in how many items they are willing to return - -1024 or 4096 are some typical values. For larger LDAP directories, you need to -either partition your results with filter, or use paged search. To get -a paged search, add the following attributes to your search request: +for example 1000 is Microsoft AD LDS default limit. To get around this limit +for larger directories, you have to use paging (as long as the server supports +it, it's an optional feature). To get paged search, add the "pagesize" attribute +to your search request: ```js search_options = { - base: '', - scope: '', - filter: '', - attrs: '', + ..., pagesize: n } -``` - -The callback will be called with a new parameter: cookie. Pass this -cookie back in subsequent searches to get the next page of results: - -```js -search_options = { - base: '', - scope: '', - filter: '', - attrs: '', - pagesize: n, - cookie: cookie +ldap.search(search_options, on_data); + +function on_data(err,data,cookie) { + // handle errors, deal with received data and... + if (cookie) { // more data available + search_options.cookie = cookie; + ldap.search(search_options, on_data); + } else { + // search is complete + } } ``` @@ -413,11 +408,5 @@ TODO Items === Basically, these are features I don't really need myself. -* Paged search results * Filter escaping -Notes on Paged Results -=== -To properly implement paged search results, we need to create another C++ class that represents the page cookie. This class should be instantiated to store the pointer to the ber cookie, and properly destroy itself when it goes out of scope. This object should be returned as part of the search results. - -[myobject.cc](https://github.com/nodejs/node-addon-examples/blob/master/8_passing_wrapped/nan/myobject.cc) seems to be a pretty good template. From f78fb44ec6747b19e1381f9d24af01a3a2fab171 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 6 Jan 2017 18:57:09 -0500 Subject: [PATCH 44/49] Add SASL bind support. Only tested with PLAIN and GSSAPI support on Linux. Requires Cyrus SASL installed at /usr/include/sasl/sasl.h. GSSAPI support tested with AD and LDS. PLAIN tested with OpenLDAP. --- LDAPCnx.cc | 22 +++++++ LDAPCnx.h | 4 ++ LDAPSASL.cc | 63 ++++++++++++++++++ LDAPXSASL.cc | 9 +++ README.md | 29 +++++++++ SASLDefaults.cc | 36 ++++++++++ SASLDefaults.h | 32 +++++++++ binding.gyp | 15 +++-- index.js | 25 ++++++- test/run_sasl.sh | 39 +++++++++++ test/sasl.conf | 26 ++++++++ test/sasl.js | 166 +++++++++++++++++++++++++++++++++++++++++++++++ test/sasl.ldif | 15 +++++ 13 files changed, 476 insertions(+), 5 deletions(-) create mode 100644 LDAPSASL.cc create mode 100644 LDAPXSASL.cc create mode 100644 SASLDefaults.cc create mode 100644 SASLDefaults.h create mode 100755 test/run_sasl.sh create mode 100644 test/sasl.conf create mode 100644 test/sasl.js create mode 100644 test/sasl.ldif diff --git a/LDAPCnx.cc b/LDAPCnx.cc index 0bf9252..d6f44cd 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -28,6 +28,7 @@ void LDAPCnx::Init(Local exports) { Nan::SetPrototypeMethod(tpl, "search", Search); Nan::SetPrototypeMethod(tpl, "delete", Delete); Nan::SetPrototypeMethod(tpl, "bind", Bind); + Nan::SetPrototypeMethod(tpl, "saslbind", SASLBind); Nan::SetPrototypeMethod(tpl, "add", Add); Nan::SetPrototypeMethod(tpl, "modify", Modify); Nan::SetPrototypeMethod(tpl, "rename", Rename); @@ -195,6 +196,27 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { break; } case LDAP_RES_BIND: + { + int msgid = ldap_msgid(message); + + if(err == LDAP_SASL_BIND_IN_PROGRESS) { + err = ld->SASLBindNext(message); + if(err != LDAP_SUCCESS) { + errparam = Nan::Error(ldap_err2string(err)); + } + else { + errparam = Nan::Undefined(); + } + } + + Local argv[] = { + errparam, + Nan::New(msgid) + }; + ld->callback->Call(2, argv); + + break; + } case LDAP_RES_MODIFY: case LDAP_RES_MODDN: case LDAP_RES_ADD: diff --git a/LDAPCnx.h b/LDAPCnx.h index 9f9c64b..0b0aff9 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -25,6 +25,7 @@ class LDAPCnx : public Nan::ObjectWrap { static void Search (const Nan::FunctionCallbackInfo& info); static void Delete (const Nan::FunctionCallbackInfo& info); static void Bind (const Nan::FunctionCallbackInfo& info); + static void SASLBind (const Nan::FunctionCallbackInfo& info); static void Add (const Nan::FunctionCallbackInfo& info); static void Modify (const Nan::FunctionCallbackInfo& info); static void Rename (const Nan::FunctionCallbackInfo& info); @@ -38,6 +39,9 @@ class LDAPCnx : public Nan::ObjectWrap { static void CheckTLS (const Nan::FunctionCallbackInfo& info); static int isBinary (char * attrname); + int SASLBindNext(LDAPMessage* result); + const char* sasl_mechanism; + ldap_conncb * ldap_callback; uv_poll_t * handle; diff --git a/LDAPSASL.cc b/LDAPSASL.cc new file mode 100644 index 0000000..1fef9ee --- /dev/null +++ b/LDAPSASL.cc @@ -0,0 +1,63 @@ +#include +#include "LDAPCnx.h" +#include "SASLDefaults.h" + +using namespace v8; + +void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { + + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + + if (ld->ld == NULL) { + Nan::ThrowError("LDAP connection has not been established"); + } + + v8::String::Utf8Value mechanism(SASLDefaults::Get(info[0])); + SASLDefaults defaults(info[1], info[2], info[3], info[4]); + v8::String::Utf8Value sec_props(SASLDefaults::Get(info[5])); + + if(*sec_props) { + int res = ldap_set_option(ld->ld, LDAP_OPT_X_SASL_SECPROPS, *sec_props); + if(res != LDAP_SUCCESS) { + Nan::ThrowError(ldap_err2string(res)); + } + } + + int msgid; + LDAPControl** sctrlsp = NULL; + LDAPMessage* message = NULL; + ld->sasl_mechanism = NULL; + + int res = ldap_sasl_interactive_bind(ld->ld, NULL, *mechanism, + sctrlsp, NULL, LDAP_SASL_QUIET, &SASLDefaults::Callback, &defaults, + message, &ld->sasl_mechanism, &msgid); + if(res != LDAP_SASL_BIND_IN_PROGRESS && res != LDAP_SUCCESS) { + Nan::ThrowError(ldap_err2string(res)); + } + + info.GetReturnValue().Set(msgid); +} + +int LDAPCnx::SASLBindNext(LDAPMessage* message) { + LDAPControl** sctrlsp = NULL; + int res; + int msgid; + do { + res = ldap_sasl_interactive_bind(ld, NULL, NULL, + sctrlsp, NULL, LDAP_SASL_QUIET, NULL, NULL, + message, &sasl_mechanism, &msgid); + + if(res != LDAP_SASL_BIND_IN_PROGRESS) { + break; + } + + if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, &message) != LDAP_SUCCESS) { + ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &res); + break; + } + + } while(res == LDAP_SASL_BIND_IN_PROGRESS); + + return res; +} + diff --git a/LDAPXSASL.cc b/LDAPXSASL.cc new file mode 100644 index 0000000..c8ff689 --- /dev/null +++ b/LDAPXSASL.cc @@ -0,0 +1,9 @@ +#include "LDAPCnx.h" + +void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { + Nan::ThrowError("LDAP module was not built with SASL support"); +} + +int LDAPCnx::SASLBindNext(LDAPMessage* result) { + return -1; +} diff --git a/README.md b/README.md index ae39500..8e6f361 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ To install the latest release from npm: You will also require the LDAP Development Libraries (on Ubuntu, `sudo apt-get install libldap2-dev`) +For SASL authentication support the Cyrus SASL libraries need to be installed +and OpenLDAP needs to be built with SASL support. + Reconnection ========== If the connection fails during operation, the client library will handle the reconnection, calling the function specified in the connect option. This callback is a good place to put bind()s and other things you want to always be in place. @@ -122,6 +125,32 @@ bind_options = { Aliased to `ldap.simplebind()` for backward compatibility. +ldap.saslbind() +=== +Upgrade the existing anonymous bind to an authenticated bind using SASL. + + ldap.saslbind([bind_options,] function(err)); + +Options are: + +* mechanism - If not provided SASL library will select based on the best + mechanism available on the server. +* user - Authentication user if required by mechanism +* password - Authentication user's password if required by mechanism +* realm - Non-default SASL realm if required by mechanism +* proxyuser - Authorization (proxy) user if supported by mechanism +* securityproperties - Optional SASL security properties + +All parameters are optional. For example a GSSAPI (Kerberos) bind can be +initiated as follows: + +``` + ldap.saslbind(function(err) { if(err) throw err; }); +``` + +For details refer to the [SASL documentation](http://cyrusimap.org/docs/cyrus-sasl). + + ldap.search() === ldap.search(search_options, function(err, data)); diff --git a/SASLDefaults.cc b/SASLDefaults.cc new file mode 100644 index 0000000..a2d8db5 --- /dev/null +++ b/SASLDefaults.cc @@ -0,0 +1,36 @@ +#include +#include "SASLDefaults.h" + +void SASLDefaults::Set(unsigned flags, sasl_interact_t *interact) { + const char *dflt = interact->defresult; + + switch (interact->id) { + case SASL_CB_AUTHNAME: + dflt = *user; + break; + case SASL_CB_PASS: + dflt = *password; + break; + case SASL_CB_GETREALM: + dflt = *realm; + break; + case SASL_CB_USER: + dflt = *proxy_user; + break; + } + + interact->result = (dflt && *dflt) ? dflt : ""; + interact->len = strlen((const char*)interact->result); +} + +int SASLDefaults::Callback(LDAP *ld, unsigned flags, void *defaults, void *in) { + SASLDefaults* self = (SASLDefaults*)defaults; + sasl_interact_t *interact = (sasl_interact_t*)in; + while(interact->id != SASL_CB_LIST_END) { + self->Set(flags, interact); + ++interact; + } + + return LDAP_SUCCESS; +} + diff --git a/SASLDefaults.h b/SASLDefaults.h new file mode 100644 index 0000000..925db65 --- /dev/null +++ b/SASLDefaults.h @@ -0,0 +1,32 @@ +#include +#include + +struct SASLDefaults { + SASLDefaults( + const v8::Local& usr, + const v8::Local& pw, + const v8::Local& rlm, + const v8::Local& proxy + ) : + user(Get(usr)), + password(Get(pw)), + realm(Get(rlm)), + proxy_user(Get(proxy)) + {} + + // Returns a C NULL value if not a string + static inline v8::Local Get(const v8::Local& v) { + return v->IsString() ? v : v8::Local(); + } + + static int Callback(LDAP *ld, unsigned flags, void *defaults, void *in); + + v8::String::Utf8Value user; + v8::String::Utf8Value password; + v8::String::Utf8Value realm; + v8::String::Utf8Value proxy_user; + +private: + void Set(unsigned flags, sasl_interact_t *interact); +}; + diff --git a/binding.gyp b/binding.gyp index 5eab7ef..e117b61 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,7 +2,8 @@ "targets": [ { "target_name": "LDAPCnx", - "sources": [ "LDAP.cc", "LDAPCnx.cc", "LDAPCookie.cc" ], + "sources": [ "LDAP.cc", "LDAPCnx.cc", "LDAPCookie.cc", + "LDAPSASL.cc", "LDAPXSASL.cc", "SASLDefaults.cc" ], "include_dirs" : [ " sasl.log 2>&1 & + +if [[ ! -f slapd.pid ]] ; then + sleep 1 +fi + +# Make sure SASL is enabled +if ldapsearch -H ldap://localhost:1234 -x -b "" -s base -LLL \ + supportedSASLMechanisms | grep -q SASL ; then + : +else + echo slapd started but SASL not supported +fi diff --git a/test/sasl.conf b/test/sasl.conf new file mode 100644 index 0000000..22cde61 --- /dev/null +++ b/test/sasl.conf @@ -0,0 +1,26 @@ +include /usr/local/etc/openldap/schema/core.schema +include /usr/local/etc/openldap/schema/cosine.schema +include /usr/local/etc/openldap/schema/inetorgperson.schema + +pidfile ./slapd.pid +argsfile ./slapd.args + +modulepath /usr/local/libexec/openldap +moduleload back_bdb + +idletimeout 100 + +database bdb + +sasl-auxprops slapd +sasl-secprops none +authz-regexp uid=(.*),cn=PLAIN,cn=auth cn=$1,dc=sample,dc=com +authz-regexp uid=(.*),cn=authz,cn=auth cn=$1,dc=sample,dc=com +password-hash {CLEARTEXT} +authz-policy from + +suffix "dc=sample,dc=com" +rootdn "cn=Manager,dc=sample,dc=com" +rootpw secret +directory ./openldap-data +index objectClass,cn,contextCSN eq diff --git a/test/sasl.js b/test/sasl.js new file mode 100644 index 0000000..e334145 --- /dev/null +++ b/test/sasl.js @@ -0,0 +1,166 @@ +/*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ + +'use strict'; + +var LDAP = require('../'); + +var assert = require('assert'); + +var ldap; + +// Does not need to support GSSAPI +var uri = process.env.TEST_SASL_URI || 'ldap://localhost:1234'; + +describe('SASL PLAIN bind', function() { + connect(uri); + it('Should bind with user', function(done) { + ldap.saslbind({ + mechanism: 'PLAIN', + user: 'test_user', + password: 'secret', + securityproperties: 'none' + }, function(err) { + assert.ifError(err); + done(); + }); + }); + search(); + after(cleanup); +}); + +describe('LDAP SASL Proxy User', function() { + connect(uri); + it('Should bind with proxy user', function(done) { + ldap.saslbind({ + mechanism: 'PLAIN', + user: 'test_user', + password: 'secret', + proxyuser: 'u:test_admin', + securityproperties: 'none' + }, function(err) { + assert.ifError(err); + done(); + }); + }); + search(); + after(cleanup); +}); + +describe('SASL Error Handling', function() { + + connect(uri); + + it('Should fail to bind invalid password', function(done) { + ldap.saslbind({ + mechanism: 'PLAIN', + user: 'test_user', + password: 'bad password', + securityproperties: 'none' + }, function(err) { + assert.ifError(!err); + done(); + }); + }); + + it('Should fail to bind invalid proxy user', function(done) { + ldap.saslbind({ + mechanism: 'PLAIN', + user: 'test_user', + password: 'secret', + proxyuser: 'no_user', + securityproperties: 'none' + }, function(err) { + assert.ifError(!err); + done(); + }); + }); + + it('Should throw on invalid mechanism', function(done) { + try { + ldap.saslbind({ mechanism: 'INVALID' }, function(err) { + assert(false); + }); + } + catch(err) { + } + done(); + }) + + it('Should throw on invalid parameter', function(done) { + try { + ldap.saslbind({realm: 0}, function(err) { + assert(false); + }); + } + catch(err) { + } + done(); + }); + + after(cleanup); +}); + +// Needs to be a server that supports SASL authentication with default +// credentials (e.g. GSSAPI) +var gssapi_uri = process.env.TEST_SASL_GSSAPI_URI; +if(gssapi_uri) { + describe('LDAP SASL GSSAPI', function() { + connect(gssapi_uri); + it('Should bind with default credentials', function(done) { + this.timeout(10000); + ldap.saslbind(function(err) { + assert.ifError(err); + done(); + }); + }); + search(); + after(cleanup); + }); +} + +function connect(uri) { + it('Should connect', function(done) { + ldap = new LDAP({ uri: uri }, function(err) { + assert.ifError(err); + done(); + }); + }); +} + +function search() { + var dc; + it('Should be able to get root info', function(done) { + ldap.search({ + base: '', + scope: LDAP.BASE, + attrs: 'namingContexts' + }, function(err, res) { + assert.ifError(err); + assert(res.length); + var ctx = res[0].namingContexts.filter(function(c) { + return c.indexOf('{') < 0; // Avoid AD config context + }); + dc = ctx[0]; + done(); + }); + }); + it('Should be able to search', function(done) { + ldap.search({ + filter: '(objectClass=*)', + base: dc, + scope: LDAP.ONELEVEL, + attrs: 'cn' + }, function(err, res) { + assert.ifError(err); + assert(res.length); + done(); + }); + }); +} + +function cleanup() { + if(ldap) { + ldap.close(); + ldap = undefined; + } +} diff --git a/test/sasl.ldif b/test/sasl.ldif new file mode 100644 index 0000000..5f12d3b --- /dev/null +++ b/test/sasl.ldif @@ -0,0 +1,15 @@ +dn: cn=test_user,dc=sample,dc=com +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: test_user +sn: test_user +userPassword: secret + +dn: cn=test_admin,dc=sample,dc=com +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: test_admin +sn: test_admin +authzFrom: u:test_user From 2747b1c3e4d78a6af019a33781bf106a3c03056f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 7 Jan 2017 08:34:53 -0500 Subject: [PATCH 45/49] Memory leak, missing ldap_msgfree on SASL binds. --- LDAPCnx.cc | 2 +- LDAPCnx.h | 2 +- LDAPSASL.cc | 13 +++++++------ LDAPXSASL.cc | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/LDAPCnx.cc b/LDAPCnx.cc index d6f44cd..4662fef 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -200,7 +200,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { int msgid = ldap_msgid(message); if(err == LDAP_SASL_BIND_IN_PROGRESS) { - err = ld->SASLBindNext(message); + err = ld->SASLBindNext(&message); if(err != LDAP_SUCCESS) { errparam = Nan::Error(ldap_err2string(err)); } diff --git a/LDAPCnx.h b/LDAPCnx.h index 0b0aff9..1fb0afe 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -39,7 +39,7 @@ class LDAPCnx : public Nan::ObjectWrap { static void CheckTLS (const Nan::FunctionCallbackInfo& info); static int isBinary (char * attrname); - int SASLBindNext(LDAPMessage* result); + int SASLBindNext(LDAPMessage** result); const char* sasl_mechanism; ldap_conncb * ldap_callback; diff --git a/LDAPSASL.cc b/LDAPSASL.cc index 1fef9ee..5f820cc 100644 --- a/LDAPSASL.cc +++ b/LDAPSASL.cc @@ -38,25 +38,26 @@ void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().Set(msgid); } -int LDAPCnx::SASLBindNext(LDAPMessage* message) { +int LDAPCnx::SASLBindNext(LDAPMessage** message) { LDAPControl** sctrlsp = NULL; int res; int msgid; - do { + while(true) { res = ldap_sasl_interactive_bind(ld, NULL, NULL, sctrlsp, NULL, LDAP_SASL_QUIET, NULL, NULL, - message, &sasl_mechanism, &msgid); + *message, &sasl_mechanism, &msgid); if(res != LDAP_SASL_BIND_IN_PROGRESS) { break; } - if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, &message) != LDAP_SUCCESS) { + ldap_msgfree(*message); + + if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, message) != LDAP_SUCCESS) { ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &res); break; } - - } while(res == LDAP_SASL_BIND_IN_PROGRESS); + } return res; } diff --git a/LDAPXSASL.cc b/LDAPXSASL.cc index c8ff689..40d21ef 100644 --- a/LDAPXSASL.cc +++ b/LDAPXSASL.cc @@ -4,6 +4,6 @@ void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { Nan::ThrowError("LDAP module was not built with SASL support"); } -int LDAPCnx::SASLBindNext(LDAPMessage* result) { +int LDAPCnx::SASLBindNext(LDAPMessage** result) { return -1; } From 1161a3c3072fd8e74171dd47aa9f4306e06a6c99 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 9 Jan 2017 11:01:36 -0500 Subject: [PATCH 46/49] SASL GSSAPI bind was not being completed. --- LDAPSASL.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LDAPSASL.cc b/LDAPSASL.cc index 5f820cc..8767dac 100644 --- a/LDAPSASL.cc +++ b/LDAPSASL.cc @@ -53,7 +53,7 @@ int LDAPCnx::SASLBindNext(LDAPMessage** message) { ldap_msgfree(*message); - if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, message) != LDAP_SUCCESS) { + if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, message) == -1) { ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &res); break; } From 2558b50b217f0d2d79510d4e49449a236b4fa126 Mon Sep 17 00:00:00 2001 From: Jeremy Childs Date: Mon, 6 Mar 2017 10:04:40 -0500 Subject: [PATCH 47/49] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e6f361..8b54b22 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The connect handler is called on initial connect as well as on reconnect, so thi var ldap = new LDAP({ uri: 'ldap://server', connect: function() { - ldap.bind({ + this.bind({ binddn: 'cn=admin,dc=com', password: 'supersecret' }, function(err) { From e237d0c1c53b0c8e1929ad615593dc41b31a5d3b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 21 Dec 2018 09:46:19 -0500 Subject: [PATCH 48/49] Node.js v10 support. --- LDAPCookie.cc | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/LDAPCookie.cc b/LDAPCookie.cc index 32f823e..1d24140 100644 --- a/LDAPCookie.cc +++ b/LDAPCookie.cc @@ -35,7 +35,8 @@ v8::Local LDAPCookie::NewInstance() { const unsigned argc = 1; v8::Local argv[argc] = { Nan::Undefined() }; v8::Local cons = Nan::New(constructor); - v8::Local instance = cons->NewInstance(argc, argv); + v8::Local instance = Nan::NewInstance(cons, argc, argv) + .ToLocalChecked(); return scope.Escape(instance); } diff --git a/package.json b/package.json index ccf3720..b6380c6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bindings": "^1.2.1", "lodash": "^3.10.1", - "nan": "^2.0.5", + "nan": "^2.12.1", "node-gyp": "" }, "engines": { From a320ca34975ecc3eff17f8df4ec06be7a7975a8e Mon Sep 17 00:00:00 2001 From: knovak Date: Tue, 14 May 2019 16:08:04 -0400 Subject: [PATCH 49/49] Removing dependence on lodash. --- index.js | 12 ++++++++++-- package.json | 1 - 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 1fc6297..bd10bca 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,6 @@ var binding = require('bindings')('LDAPCnx'); var LDAPError = require('./LDAPError'); var assert = require('assert'); var util = require('util'); -var _ = require('lodash'); function arg(val, def) { if (val !== undefined) { @@ -15,6 +14,15 @@ function arg(val, def) { 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), @@ -65,7 +73,7 @@ function LDAP(opt, fn) { this.queue = {}; this.stats = new Stats(); - this.options = _.assign({ + this.options = extendobj({ base: 'dc=com', filter: '(objectClass=*)', scope: 2, diff --git a/package.json b/package.json index b6380c6..ae0c277 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ }, "dependencies": { "bindings": "^1.2.1", - "lodash": "^3.10.1", "nan": "^2.12.1", "node-gyp": "" },