diff --git a/._.DS_Store b/._.DS_Store new file mode 100644 index 0000000..043a462 Binary files /dev/null and b/._.DS_Store differ diff --git a/.gitignore b/.gitignore index 6124299..7eaa228 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ openldap-data test/slapd.args test/slapd.pid test/reconnect.js +.DS_Store +.fuse_* +*.log 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 9ec7f51..4662fef 100644 --- a/LDAPCnx.cc +++ b/LDAPCnx.cc @@ -1,4 +1,5 @@ #include "LDAPCnx.h" +#include "LDAPCookie.h" static struct timeval ldap_tv = { 0, 0 }; @@ -27,13 +28,18 @@ 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); - Nan::SetPrototypeMethod(tpl, "initialize", Initialize); + Nan::SetPrototypeMethod(tpl, "abandon", Abandon); Nan::SetPrototypeMethod(tpl, "errorstring", GetErr); - Nan::SetPrototypeMethod(tpl, "errorno", GetErrNo); + 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()); exports->Set(Nan::New("LDAPCnx").ToLocalChecked(), tpl->GetFunction()); @@ -49,6 +55,38 @@ 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 referrals = info[7]->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_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; @@ -62,15 +100,15 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { LDAPMessage * message = NULL; LDAPMessage * entry = NULL; Local errparam; + + 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); @@ -79,8 +117,7 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { } else { 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 +142,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"); - int bin = !strcmp(attrname, "jpegPhoto"); + // TODO: check for binary settings + int bin = isBinary(attrname); 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()); @@ -123,20 +159,69 @@ 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; } 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: case LDAP_RES_DELETE: + case LDAP_RES_EXTENDED: { Local argv[] = { errparam, @@ -147,7 +232,6 @@ void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { } default: { - //emit an error // Nan::ThrowError("Unrecognized packet"); } @@ -158,11 +242,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,58 +267,59 @@ void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx) { // this fires when the connection closes LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; + if (lc->handle) { + 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(); - - 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; +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! - if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { - Nan::ThrowError("Error init"); - return; - } + return LDAP_SUCCESS; +} - struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; +void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + int err; + ldap_get_option(ld->ld, LDAP_OPT_RESULT_CODE, &err); + info.GetReturnValue().Set(Nan::New(ldap_err2string(err)).ToLocalChecked()); +} - 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); +void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - if (starttls == 1) { - ldap_start_tls_s(ld->ld, NULL, NULL); - } + info.GetReturnValue().Set(ldap_unbind(ld->ld)); +} + +void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); + int msgid; + int res; - if ((ldap_simple_bind(ld->ld, NULL, NULL)) == -1) { - Nan::ThrowError("Error anon bind"); - return; - } + res = ldap_start_tls(ld->ld, NULL, NULL, &msgid); + + info.GetReturnValue().Set(msgid); +} - ldap_get_option(ld->ld, LDAP_OPT_DESC, &fd); - - if (fd < 0) { - Nan::ThrowError("Connection issue"); - return; - } +void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { + LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - info.GetReturnValue().Set(info.This()); + info.GetReturnValue().Set(ldap_install_tls(ld->ld)); } -void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { +void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); - int err; - ldap_get_option(ld->ld, LDAP_OPT_RESULT_CODE, &err); - info.GetReturnValue().Set(Nan::New(ldap_err2string(err)).ToLocalChecked()); + + 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())); } void LDAPCnx::GetErrNo(const Nan::FunctionCallbackInfo& info) { @@ -268,7 +348,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) { @@ -288,6 +370,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]; @@ -300,8 +384,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); @@ -384,6 +484,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); } @@ -398,3 +499,30 @@ 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") || + !strcmp(attrname, "objectSid") || + strstr(attrname, ";binary")) { + return 1; + } + return 0; +} diff --git a/LDAPCnx.h b/LDAPCnx.h index 6a84f46..1fb0afe 100644 --- a/LDAPCnx.h +++ b/LDAPCnx.h @@ -15,25 +15,39 @@ 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 GetErr(const Nan::FunctionCallbackInfo& info); - static void GetErrNo(const Nan::FunctionCallbackInfo& info); - static void GetFD(const Nan::FunctionCallbackInfo& info); + 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); + 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); + 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 int isBinary (char * attrname); + + int SASLBindNext(LDAPMessage** result); + const char* sasl_mechanism; + ldap_conncb * ldap_callback; uv_poll_t * handle; - + static Nan::Persistent constructor; LDAP * ld; }; #endif + diff --git a/LDAPCookie.cc b/LDAPCookie.cc new file mode 100644 index 0000000..1d24140 --- /dev/null +++ b/LDAPCookie.cc @@ -0,0 +1,43 @@ +#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 = Nan::NewInstance(cons, argc, argv) + .ToLocalChecked(); + + 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/LDAPSASL.cc b/LDAPSASL.cc new file mode 100644 index 0000000..8767dac --- /dev/null +++ b/LDAPSASL.cc @@ -0,0 +1,64 @@ +#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; + while(true) { + 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; + } + + ldap_msgfree(*message); + + if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, message) == -1) { + ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &res); + break; + } + } + + return res; +} + diff --git a/LDAPXSASL.cc b/LDAPXSASL.cc new file mode 100644 index 0000000..40d21ef --- /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 666da42..8b54b22 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,24 +6,24 @@ 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. +***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 @@ -35,15 +35,25 @@ 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`) +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. + +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 === - new LDAP(options); + new LDAP(options, readyCallback); Options are provided as a JS object: @@ -52,41 +62,57 @@ 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 + connect: function(), // optional function to call when connect/reconnect occurs disconnect: function(), // optional function to call when disconnect occurs +}, function(err) { + // connected and ready }); ``` -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 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. -ldap.open() ------------ +```js +var ldap = new LDAP({ + uri: 'ldap://server', + connect: function() { + this.bind({ + binddn: 'cn=admin,dc=com', + password: 'supersecret' + }, function(err) { + ... + }); + } +} +``` -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 set the `verifycert` attribute to `LDAP.LDAP_OPT_X_TLS_NEVER`, or one of the following values: ```js -ldap.open(function(err) { - if (err) { - // will never happen - } - // connection is ready. -}); +var LDAP=require('ldap-client'); -ldap.simplebind() ------------------ +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.simplebind(bind_options, function(err)); + ldap.bind(bind_options, function(err)); Options are binddn and password: @@ -96,29 +122,63 @@ bind_options = { password: '' } ``` +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)); 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 @@ -133,6 +193,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' ], @@ -145,54 +206,52 @@ 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 +=== 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 + } } ``` +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)) @@ -219,13 +278,47 @@ 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() ----------- +=== ldap.add(dn, [attrs], function(err)) @@ -241,7 +334,7 @@ var attrs = [ ``` ldap.modify() -------------- +=== ldap.modify(dn, [ changes ], function(err)) @@ -258,7 +351,7 @@ var changes = [ ``` ldap.rename() -------------- +=== ldap.rename(dn, newrdn, function(err)) @@ -271,7 +364,7 @@ ldap.rename('cn=name,dc=example,dc=com', 'cn=newname') ``` ldap.remove() -------------- +=== ldap.remove(dn, function(err)) @@ -287,18 +380,62 @@ 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, `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" + +``` + 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 -* close() and friends -* test starttls +* Filter escaping + 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 04efb4d..e117b61 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,7 +2,8 @@ "targets": [ { "target_name": "LDAPCnx", - "sources": [ "LDAP.cc", "LDAPCnx.cc" ], + "sources": [ "LDAP.cc", "LDAPCnx.cc", "LDAPCookie.cc", + "LDAPSASL.cc", "LDAPXSASL.cc", "SASLDefaults.cc" ], "include_dirs" : [ "|=|\\/g), + replacements: { + "\0": "\\00", + " ": "\\ ", + "\"": "\\\"", + "#": "\\#", + "+": "\\+", + ",": "\\,", + ";": "\\;", + "<": "\\<", + ">": "\\>", + "=": "\\=", + "\\": "\\5C" + } + } +}; + function Stats() { this.lateresponses = 0; this.reconnects = 0; @@ -29,68 +69,64 @@ function Stats() { return this; } -function LDAP(opt) { - this.callbacks = {}; - this.defaults = { - base: 'dc=com', - filter: '(objectClass=*)', - scope: this.SUBTREE, - attrs: '*', - starttls: false, - ntimeout: 1000 - }; - this.timeout = 2000; - +function LDAP(opt, fn) { + this.queue = {}; this.stats = new Stats(); - if (typeof opt.reconnect === 'function') { - this.onreconnect = opt.reconnect; - } - if (typeof opt.disconnect === 'function') { - this.ondisconnect = opt.disconnect; - } + this.options = extendobj({ + base: 'dc=com', + filter: '(objectClass=*)', + scope: 2, + attrs: '*', + ntimeout: 1000, + timeout: 2000, + debug: 0, + validatecert: LDAP.LDAP_OPT_X_TLS_HARD, + referrals: 0, + connect: function() {}, + disconnect: function() {} + }, opt); - if (typeof opt.uri !== 'string') { - throw new LDAPError('Missing argument'); + if (typeof this.options.uri === 'string') { + this.options.uri = [ this.options.uri ]; } - 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.ondisconnect.bind(this)); - try { - this.ld.initialize(this.defaults.uri, this.defaults.ntimeout, this.defaults.starttls); - } catch (e) { - - } - return this; -} -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++; + this.ld = new binding.LDAPCnx(this.dequeue.bind(this), + this.onconnect.bind(this), + this.ondisconnect.bind(this), + this.options.uri.join(' '), + this.options.ntimeout, + this.options.debug, + this.options.validatecert, + this.options.referrals); + + if (typeof fn !== 'function') { + fn = function() {}; } -}; -LDAP.prototype.onreconnect = function() { + return this.enqueue(this.ld.bind(undefined, undefined), fn); +} + +LDAP.prototype.onconnect = function() { this.stats.reconnects++; - // default reconnect callback does nothing + return this.options.connect.call(this); }; LDAP.prototype.ondisconnect = function() { this.stats.disconnects++; - // default reconnect callback does nothing + this.options.disconnect(); +}; + +LDAP.prototype.starttls = function(fn) { + return this.enqueue(this.ld.starttls(), fn); +}; + +LDAP.prototype.installtls = function() { + return this.ld.installtls(); +}; + +LDAP.prototype.tlsactive = function() { + return this.ld.checktls(); }; LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { @@ -113,6 +149,29 @@ LDAP.prototype.bind = LDAP.prototype.simplebind = function(opt, fn) { return this.enqueue(this.ld.bind(opt.binddn, opt.password), fn); }; +LDAP.prototype.saslbind = function(opt, fn) { + + this.stats.binds++; + + if(arguments.length == 1 && typeof arguments[0] === 'function') { + fn = opt; + opt = undefined; + } + + var args = [ + 'mechanism','user','password','realm','proxyuser','securityproperties' + ].map(function(p) { return opt == null ? undefined : opt[p]; }); + + if (args.filter(function(a) { + return a != null && typeof a !== 'string'; + }).length || + typeof fn !== 'function') { + throw new LDAPError('Invalid argument'); + } + + return this.enqueue(this.ld.saslbind.apply(this.ld, args), fn); +}; + LDAP.prototype.add = function(dn, attrs, fn) { this.stats.adds++; if (typeof dn !== 'string' || @@ -124,10 +183,16 @@ 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), + arg(opt.pagesize, this.options.pagesize), + arg(opt.cookie, null) + ), unwrap_cookie); + function unwrap_cookie(err, data) { + err ? fn(err) : fn(err, data.data, data.cookie); + } }; LDAP.prototype.rename = function(dn, newrdn, fn) { @@ -156,58 +221,117 @@ LDAP.prototype.findandbind = function(opt, fn) { throw new Error('Missing argument'); } - this.search(opt, function(err, data) { - if (err) { - fn(err); - return; - } + this.search(opt, function findandbindFind(err, data) { + 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.defaults); + this.auth_connection = new LDAP(this.options, function newAuthConnection(err) { + if (err) return fn(err); + return this.authbind(data[0].dn, opt.password, function authbindResult(err) { + fn(err, data[0]); + }); + }.bind(this)); + } else { + this.authbind(data[0].dn, opt.password, function authbindResult(err) { + fn(err, data[0]); + }); } - 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(); } - // TODO: clean up and disconnect + this.ld.close(); + 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) { - process.nextTick(function() { + if (msgid == -1 || this.ld === undefined) { + if (this.ld.errorstring() === 'Can\'t contact LDAP server') { + // this means we have had a disconnect event, but since there + // are still requests outstanding from libldap's perspective, + // the connection isn't "closed" and the disconnect event has + // not yet fired. To get libldap to actually call the disconnect + // handler, we need to dump all outstanding requests, and hope + // we're not missing one for some reason. Only once we've + // abandoned everything does the handle properly close. + Object.keys(this.queue).forEach(function fireTimeout(msgid) { + this.queue[msgid](new LDAPError('Timeout')); + delete this.queue[msgid]; + this.ld.abandon(msgid); + }.bind(this)); + } + process.nextTick(function emitError() { fn(new LDAPError(this.ld.errorstring())); - return; }.bind(this)); this.stats.errors++; return this; } fn.timer = setTimeout(function searchTimeout() { - delete this.callbacks[msgid]; - fn(new LDAPError('Timeout'), msgid); + this.ld.abandon(msgid); + delete this.queue[msgid]; + fn(new LDAPError('Timeout')); this.stats.timeouts++; - }.bind(this), this.timeout); - this.callbacks[msgid] = fn; + }.bind(this), this.options.timeout); + this.queue[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; +function stringescape(escapes_obj, str) { + return str.replace(escapes_obj.regex, function (match) { + return escapes_obj.replacements[match]; + }); +} + +LDAP.escapefn = function(type, template) { + var escapes_obj = escapes[type]; + return function() { + var args = [ template ], i; + for (i = 0 ; i < arguments.length ; i++) { // optimizer-friendly + args.push(stringescape(escapes_obj, arguments[i])); + } + return util.format.apply(this,args); + }; +}; + +LDAP.stringEscapeFilter = LDAP.escapefn('filter', '%s'); + +function setConst(target, name, val) { + target.prototype[name] = target[name] = val; +} + +setConst(LDAP, 'BASE', 0); +setConst(LDAP, 'ONELEVEL', 1); +setConst(LDAP, 'SUBTREE', 2); +setConst(LDAP, 'SUBORDINATE', 3); +setConst(LDAP, 'DEFAULT', 4); + +setConst(LDAP, 'LDAP_OPT_X_TLS_NEVER', 0); +setConst(LDAP, 'LDAP_OPT_X_TLS_HARD', 1); +setConst(LDAP, 'LDAP_OPT_X_TLS_DEMAND', 2); +setConst(LDAP, 'LDAP_OPT_X_TLS_ALLOW', 3); +setConst(LDAP, 'LDAP_OPT_X_TLS_TRY', 4); module.exports = LDAP; diff --git a/package.json b/package.json index 941075d..ae0c277 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,11 @@ }, "dependencies": { "bindings": "^1.2.1", - "nan": "^2.0.5", - "node-gyp": "^1.0.3" + "nan": "^2.12.1", + "node-gyp": "" }, "engines": { "node": ">= 0.8.0" }, - "version": "2.0.7" + "version": "3.1.3" } diff --git a/test/certs/._.DS_Store b/test/certs/._.DS_Store new file mode 100644 index 0000000..70d3a37 Binary files /dev/null and b/test/certs/._.DS_Store differ 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/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)'); + }); +}); diff --git a/test/index.js b/test/index.js index 15b5ea4..c9756f1 100644 --- a/test/index.js +++ b/test/index.js @@ -6,6 +6,7 @@ var LDAP = require('../'); var assert = require('assert'); var fs = require('fs'); var ldap; +var ldap2; // This shows an inline image for iTerm2 // should not harm anything otherwise. @@ -18,21 +19,20 @@ function showImage(what) { } - describe('LDAP', function() { - it ('Should initialize OK', function() { + it ('Should initialize OK', function(done) { ldap = new LDAP({ uri: 'ldap://localhost:1234', base: 'dc=sample,dc=com', attrs: '*' - }); + }, done); }); it ('Should search', function(done) { ldap.search({ 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'); @@ -40,19 +40,23 @@ 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); + }); it ('Should return specified attrs', function(done) { ldap.search({ base: 'dc=sample,dc=com', 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'); @@ -64,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); @@ -72,7 +77,7 @@ describe('LDAP', function() { }); 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(); }); }); @@ -83,7 +88,8 @@ describe('LDAP', function() { attrs: '*', password: 'foobarbaz' }, function(err, data) { - assert.equal(err, undefined); + assert.ifError(err); + assert.equal(data.cn, 'Charlie'); done(); }); }); @@ -94,7 +100,8 @@ describe('LDAP', function() { attrs: '*', password: 'foobarbaz' }, function(err, data) { - assert.equal(err, undefined); + assert.ifError(err); + assert.equal(data.cn, 'Charlie'); done(); }); }); @@ -105,25 +112,37 @@ describe('LDAP', function() { 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 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.equal(err, undefined); + assert.ifError(err); ldap.search({ base: 'dc=sample,dc=com', filter: '(cn=albert)', @@ -153,7 +172,7 @@ describe('LDAP', function() { vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] } ], function(err, res) { - assert.equal(err, undefined); + assert.ifError(err); done(); }); @@ -177,11 +196,12 @@ describe('LDAP', function() { vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] } ], function(err, res) { - assert.notEqual(err, undefined); + assert.ifError(!err); done(); }); }); it ('Should survive a slight beating', function(done) { + this.timeout(5000); var count = 0; for (var x = 0 ; x < 1000 ; x++) { ldap.search({ @@ -198,16 +218,16 @@ describe('LDAP', function() { }); 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(); }); }); @@ -216,7 +236,7 @@ describe('LDAP', function() { { 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', @@ -253,28 +273,78 @@ 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(); - }); - }); + 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) { + it ('Should search with weird inputs', function(done) { ldap.search({ base: 'dc=sample,dc=com', - filter: '(title=ᓄᓇᕗᑦ ᒐᕙᒪᖓ)', + scope: LDAP.ONELEVEL, + filter: '(objectClass=*)', + attrs: '+' + }, function(err, res) { + assert.equal(res.length, 4); + done(); + }); + }); + it ('Should close and disconnect', function() { + ldap.close(); + }); + 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() { + ldap.close(); + }); + 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) { - assert.equal(res[0].dn, 'cn=Albert,ou=Accounting,dc=sample,dc=com'); - done(); + count++; + if (count >= 1000) { + ldap.close(); + done(); + } }); + } }); }); diff --git a/test/issues.js b/test/issues.js new file mode 100644 index 0000000..6b569f9 --- /dev/null +++ b/test/issues.js @@ -0,0 +1,83 @@ +/*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; + +var ldapConfig = { + schema: 'ldaps://', + host: 'localhost:1235' +}; +var uri = ldapConfig.schema + ldapConfig.host; + + +describe('Issues', function() { + it('Should fix Issue #80', function(done) { + ldap = new LDAP({ + uri: uri, + validatecert: LDAP.LDAP_OPT_X_TLS_NEVER + }, 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('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'); + 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); + 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/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/leakcheck b/test/leakcheck index 5173f7e..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://10.0.0.1:1234', connecttimeout: 100}); +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_sasl.sh b/test/run_sasl.sh new file mode 100755 index 0000000..2c51333 --- /dev/null +++ b/test/run_sasl.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +if [[ -z $SLAPD ]] ; then + SLAPD=/usr/local/libexec/slapd +fi + +if [[ -z $SLAPADD ]] ; then + SLAPADD=/usr/local/sbin/slapadd +fi + +if [[ -z $SLAPD_CONF ]] ; then + SLAPD_CONF=sasl.conf +fi + +MKDIR=/bin/mkdir +RM=/bin/rm + +$RM -rf openldap-data +$MKDIR openldap-data + +if [[ -f slapd.pid ]] ; then + $RM slapd.pid +fi + +$SLAPADD -f $SLAPD_CONF < startup.ldif +$SLAPADD -f $SLAPD_CONF < sasl.ldif +$SLAPD -d999 -f $SLAPD_CONF -hldap://localhost:1234 > 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/run_server.sh b/test/run_server.sh index 6d88814..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 -d1 -f slapd.conf -hldap://localhost:1234 +$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/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 diff --git a/test/slapd.conf b/test/slapd.conf index b0e3a56..dc854fd 100644 --- a/test/slapd.conf +++ b/test/slapd.conf @@ -7,6 +7,10 @@ include /usr/local/etc/openldap/schema/cosine.schema include /usr/local/etc/openldap/schema/inetorgperson.schema # Define global ACLs to disable default read access. +TLSCACertificateFile certs/rootCA.pem +TLSCertificateFile certs/device.crt +TLSCertificateKeyFile certs/device.key + # Do not enable referrals until AFTER you have a working directory # service AND an understanding of referrals. #referral ldap://root.openldap.org @@ -16,10 +20,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 @@ -34,12 +45,12 @@ moduleload back_bdb # 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 @@ -53,7 +64,7 @@ idletimeout 100 # BDB database definitions ####################################################################### -database bdb +database mdb # overlay syncprov # syncprov-checkpoint 10 10 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 diff --git a/test/tls.js b/test/tls.js new file mode 100644 index 0000000..061f2ab --- /dev/null +++ b/test/tls.js @@ -0,0 +1,85 @@ +/*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() { + /* + 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: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*' + }, function(err) { + 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: 'ldap://localhost:1234', + base: 'dc=sample,dc=com', + attrs: '*', + validatecert: false + }, function(err) { + assert.ifError(err); + ldap.starttls(function(err) { + assert.ifError(err); + ldap.installtls(); + assert(ldap.tlsactive()); + 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()); + ldap.close(); + }); +});