├── ._.DS_Store ├── .gitignore ├── LDAP.cc ├── LDAPCnx.cc ├── LDAPCnx.h ├── LDAPCookie.cc ├── LDAPCookie.h ├── LDAPError.js ├── LDAPSASL.cc ├── LDAPXSASL.cc ├── LICENCE ├── README.md ├── SASLDefaults.cc ├── SASLDefaults.h ├── binding.gyp ├── index.js ├── package.json └── test ├── .gitignore ├── README.md ├── add.ldif ├── add2.ldif ├── certs ├── ._.DS_Store ├── device.crt ├── device.csr ├── device.key ├── rootCA.key ├── rootCA.pem └── rootCA.srl ├── escaping.js ├── index.js ├── issues.js ├── ldaps.js ├── leakcheck ├── run_sasl.sh ├── run_server.sh ├── sasl.conf ├── sasl.js ├── sasl.ldif ├── slapd.conf ├── startup.ldif └── tls.js /._.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycx/node-LDAP/65029c70c6140de0460b3c3df403ce3fc5d196fa/._.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.core 2 | *.swo 3 | *.swp 4 | *~ 5 | .lock-wscript 6 | .project 7 | .settings 8 | /node_modules 9 | /sn.txt 10 | /test-inflight.js 11 | /test.js 12 | /test2.js 13 | /v8.log 14 | build 15 | ldap.node 16 | *#* 17 | npm-debug.log 18 | sample.c 19 | .tern-port 20 | openldap-data 21 | test/slapd.args 22 | test/slapd.pid 23 | test/reconnect.js 24 | .DS_Store 25 | .fuse_* 26 | *.log 27 | -------------------------------------------------------------------------------- /LDAP.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "LDAPCnx.h" 3 | #include "LDAPCookie.h" 4 | 5 | void InitAll(v8::Local exports) { 6 | LDAPCnx::Init(exports); 7 | LDAPCookie::Init(exports); 8 | } 9 | 10 | NODE_MODULE(LDAPCnx, InitAll) 11 | -------------------------------------------------------------------------------- /LDAPCnx.cc: -------------------------------------------------------------------------------- 1 | #include "LDAPCnx.h" 2 | #include "LDAPCookie.h" 3 | 4 | static struct timeval ldap_tv = { 0, 0 }; 5 | 6 | using namespace v8; 7 | 8 | Nan::Persistent LDAPCnx::constructor; 9 | 10 | LDAPCnx::LDAPCnx() { 11 | } 12 | 13 | LDAPCnx::~LDAPCnx() { 14 | free(this->ldap_callback); 15 | delete this->callback; 16 | delete this->reconnect_callback; 17 | } 18 | 19 | void LDAPCnx::Init(Local exports) { 20 | Nan::HandleScope scope; 21 | 22 | // Prepare constructor template 23 | Local tpl = Nan::New(New); 24 | tpl->SetClassName(Nan::New("LDAPCnx").ToLocalChecked()); 25 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 26 | 27 | // Prototype 28 | Nan::SetPrototypeMethod(tpl, "search", Search); 29 | Nan::SetPrototypeMethod(tpl, "delete", Delete); 30 | Nan::SetPrototypeMethod(tpl, "bind", Bind); 31 | Nan::SetPrototypeMethod(tpl, "saslbind", SASLBind); 32 | Nan::SetPrototypeMethod(tpl, "add", Add); 33 | Nan::SetPrototypeMethod(tpl, "modify", Modify); 34 | Nan::SetPrototypeMethod(tpl, "rename", Rename); 35 | Nan::SetPrototypeMethod(tpl, "abandon", Abandon); 36 | Nan::SetPrototypeMethod(tpl, "errorstring", GetErr); 37 | Nan::SetPrototypeMethod(tpl, "close", Close); 38 | Nan::SetPrototypeMethod(tpl, "errno", GetErrNo); 39 | Nan::SetPrototypeMethod(tpl, "fd", GetFD); 40 | Nan::SetPrototypeMethod(tpl, "installtls", InstallTLS); 41 | Nan::SetPrototypeMethod(tpl, "starttls", StartTLS); 42 | Nan::SetPrototypeMethod(tpl, "checktls", CheckTLS); 43 | 44 | constructor.Reset(tpl->GetFunction()); 45 | exports->Set(Nan::New("LDAPCnx").ToLocalChecked(), tpl->GetFunction()); 46 | } 47 | 48 | void LDAPCnx::New(const Nan::FunctionCallbackInfo& info) { 49 | if (info.IsConstructCall()) { 50 | // Invoked as constructor: `new LDAPCnx(...)` 51 | LDAPCnx* ld = new LDAPCnx(); 52 | ld->Wrap(info.Holder()); 53 | 54 | ld->callback = new Nan::Callback(info[0].As()); 55 | ld->reconnect_callback = new Nan::Callback(info[1].As()); 56 | ld->disconnect_callback = new Nan::Callback(info[2].As()); 57 | ld->handle = NULL; 58 | 59 | Nan::Utf8String url(info[3]); 60 | int ver = LDAP_VERSION3; 61 | int timeout = info[4]->NumberValue(); 62 | int debug = info[5]->NumberValue(); 63 | int verifycert = info[6]->NumberValue(); 64 | int referrals = info[7]->NumberValue(); 65 | int zero = 0; 66 | 67 | ld->ldap_callback = (ldap_conncb *)malloc(sizeof(ldap_conncb)); 68 | ld->ldap_callback->lc_add = OnConnect; 69 | ld->ldap_callback->lc_del = OnDisconnect; 70 | ld->ldap_callback->lc_arg = ld; 71 | 72 | if (ldap_initialize(&(ld->ld), *url) != LDAP_SUCCESS) { 73 | Nan::ThrowError("Error init"); 74 | return; 75 | } 76 | 77 | struct timeval ntimeout = { timeout/1000, (timeout%1000) * 1000 }; 78 | 79 | ldap_set_option(ld->ld, LDAP_OPT_PROTOCOL_VERSION, &ver); 80 | ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug); 81 | ldap_set_option(ld->ld, LDAP_OPT_CONNECT_CB, ld->ldap_callback); 82 | ldap_set_option(ld->ld, LDAP_OPT_NETWORK_TIMEOUT, &ntimeout); 83 | ldap_set_option(ld->ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &verifycert); 84 | ldap_set_option(ld->ld, LDAP_OPT_X_TLS_NEWCTX, &zero); 85 | 86 | ldap_set_option(ld->ld, LDAP_OPT_REFERRALS, &referrals); 87 | if (referrals) { 88 | ldap_set_rebind_proc(ld->ld, OnRebind, ld); 89 | } 90 | 91 | info.GetReturnValue().Set(info.Holder()); 92 | return; 93 | } 94 | Nan::ThrowError("Must instantiate with new"); 95 | } 96 | 97 | void LDAPCnx::Event(uv_poll_t* handle, int status, int events) { 98 | Nan::HandleScope scope; 99 | LDAPCnx *ld = (LDAPCnx *)handle->data; 100 | LDAPMessage * message = NULL; 101 | LDAPMessage * entry = NULL; 102 | Local errparam; 103 | 104 | int msgtype; 105 | 106 | switch(ldap_result(ld->ld, LDAP_RES_ANY, LDAP_MSG_ALL, &ldap_tv, &message)) { 107 | case 0: 108 | // timeout occurred, which I don't think happens in async mode 109 | case -1: 110 | // We can't really do much; we don't have a msgid to callback to 111 | break; 112 | default: 113 | { 114 | int err = ldap_result2error(ld->ld, message, 0); 115 | if (err) { 116 | errparam = Nan::Error(ldap_err2string(err)); 117 | } else { 118 | errparam = Nan::Undefined(); 119 | } 120 | switch ( msgtype = ldap_msgtype( message ) ) { 121 | case LDAP_RES_SEARCH_REFERENCE: 122 | break; 123 | case LDAP_RES_SEARCH_ENTRY: 124 | case LDAP_RES_SEARCH_RESULT: 125 | { 126 | Local js_result_list = Nan::New(ldap_count_entries(ld->ld, message)); 127 | 128 | int j; 129 | 130 | for (entry = ldap_first_entry(ld->ld, message), j = 0 ; entry ; 131 | entry = ldap_next_entry(ld->ld, entry), j++) { 132 | Local js_result = Nan::New(); 133 | 134 | js_result_list->Set(Nan::New(j), js_result); 135 | 136 | char * dn = ldap_get_dn(ld->ld, entry); 137 | BerElement * berptr = NULL; 138 | for (char * attrname = ldap_first_attribute(ld->ld, entry, &berptr) ; 139 | attrname ; attrname = ldap_next_attribute(ld->ld, entry, berptr)) { 140 | berval ** vals = ldap_get_values_len(ld->ld, entry, attrname); 141 | int num_vals = ldap_count_values_len(vals); 142 | Local js_attr_vals = Nan::New(num_vals); 143 | js_result->Set(Nan::New(attrname).ToLocalChecked(), js_attr_vals); 144 | 145 | // TODO: check for binary settings 146 | int bin = isBinary(attrname); 147 | 148 | for (int i = 0 ; i < num_vals && vals[i] ; i++) { 149 | if (bin) { 150 | js_attr_vals->Set(Nan::New(i), Nan::CopyBuffer(vals[i]->bv_val, vals[i]->bv_len).ToLocalChecked()); 151 | } else { 152 | js_attr_vals->Set(Nan::New(i), Nan::New(vals[i]->bv_val).ToLocalChecked()); 153 | } 154 | } // all values for this attr added. 155 | ldap_value_free_len(vals); 156 | ldap_memfree(attrname); 157 | } // attrs for this entry added. Next entry. 158 | js_result->Set(Nan::New("dn").ToLocalChecked(), Nan::New(dn).ToLocalChecked()); 159 | ber_free(berptr,0); 160 | ldap_memfree(dn); 161 | } // all entries done. 162 | 163 | Local result_container = Nan::New(); 164 | result_container->Set(Nan::New("data").ToLocalChecked(), js_result_list); 165 | 166 | LDAPControl** serverCtrls; 167 | ldap_parse_result(ld->ld, message, 168 | NULL, // int* errcodep 169 | NULL, // char** matcheddnp 170 | NULL, // char** errmsp 171 | NULL, // char*** referralsp 172 | &serverCtrls, 173 | 0 // freeit 174 | ); 175 | if (serverCtrls) { 176 | struct berval* cookie = NULL; 177 | ldap_parse_page_control(ld->ld, serverCtrls, NULL, &cookie); 178 | if (!cookie || cookie->bv_val == NULL || !*cookie->bv_val) { 179 | if (cookie) 180 | ber_bvfree(cookie); 181 | } else { 182 | Local cookieWrap = LDAPCookie::NewInstance(); 183 | LDAPCookie* cookieContainer = ObjectWrap::Unwrap(cookieWrap); 184 | cookieContainer->SetCookie(cookie); 185 | result_container->Set(Nan::New("cookie").ToLocalChecked(), cookieWrap); 186 | } 187 | ldap_controls_free(serverCtrls); 188 | } 189 | 190 | Local argv[] = { 191 | errparam, 192 | Nan::New(ldap_msgid(message)), 193 | result_container 194 | }; 195 | ld->callback->Call(3, argv); 196 | break; 197 | } 198 | case LDAP_RES_BIND: 199 | { 200 | int msgid = ldap_msgid(message); 201 | 202 | if(err == LDAP_SASL_BIND_IN_PROGRESS) { 203 | err = ld->SASLBindNext(&message); 204 | if(err != LDAP_SUCCESS) { 205 | errparam = Nan::Error(ldap_err2string(err)); 206 | } 207 | else { 208 | errparam = Nan::Undefined(); 209 | } 210 | } 211 | 212 | Local argv[] = { 213 | errparam, 214 | Nan::New(msgid) 215 | }; 216 | ld->callback->Call(2, argv); 217 | 218 | break; 219 | } 220 | case LDAP_RES_MODIFY: 221 | case LDAP_RES_MODDN: 222 | case LDAP_RES_ADD: 223 | case LDAP_RES_DELETE: 224 | case LDAP_RES_EXTENDED: 225 | { 226 | Local argv[] = { 227 | errparam, 228 | Nan::New(ldap_msgid(message)) 229 | }; 230 | ld->callback->Call(2, argv); 231 | break; 232 | } 233 | default: 234 | { 235 | //emit an error 236 | // Nan::ThrowError("Unrecognized packet"); 237 | } 238 | } 239 | } 240 | } 241 | ldap_msgfree(message); 242 | return; 243 | } 244 | 245 | int LDAPCnx::OnConnect(LDAP *ld, Sockbuf *sb, 246 | LDAPURLDesc *srv, struct sockaddr *addr, 247 | struct ldap_conncb *ctx) { 248 | int fd; 249 | LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; 250 | 251 | if (lc->handle == NULL) { 252 | lc->handle = new uv_poll_t; 253 | ldap_get_option(ld, LDAP_OPT_DESC, &fd); 254 | uv_poll_init(uv_default_loop(), lc->handle, fd); 255 | lc->handle->data = lc; 256 | } else { 257 | uv_poll_stop(lc->handle); 258 | } 259 | uv_poll_start(lc->handle, UV_READABLE, (uv_poll_cb)lc->Event); 260 | 261 | lc->reconnect_callback->Call(0, NULL); 262 | 263 | return LDAP_SUCCESS; 264 | } 265 | 266 | void LDAPCnx::OnDisconnect(LDAP *ld, Sockbuf *sb, 267 | struct ldap_conncb *ctx) { 268 | // this fires when the connection closes 269 | LDAPCnx * lc = (LDAPCnx *)ctx->lc_arg; 270 | if (lc->handle) { 271 | uv_poll_stop(lc->handle); 272 | } 273 | lc->disconnect_callback->Call(0, NULL); 274 | } 275 | 276 | int LDAPCnx::OnRebind(LDAP *ld, LDAP_CONST char *url, ber_tag_t request, 277 | ber_int_t msgid, void *params) { 278 | // this is a new *ld representing the new server connection 279 | // so our existing code won't work! 280 | 281 | return LDAP_SUCCESS; 282 | } 283 | 284 | void LDAPCnx::GetErr(const Nan::FunctionCallbackInfo& info) { 285 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 286 | int err; 287 | ldap_get_option(ld->ld, LDAP_OPT_RESULT_CODE, &err); 288 | info.GetReturnValue().Set(Nan::New(ldap_err2string(err)).ToLocalChecked()); 289 | } 290 | 291 | void LDAPCnx::Close(const Nan::FunctionCallbackInfo& info) { 292 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 293 | 294 | info.GetReturnValue().Set(ldap_unbind(ld->ld)); 295 | } 296 | 297 | void LDAPCnx::StartTLS(const Nan::FunctionCallbackInfo& info) { 298 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 299 | int msgid; 300 | int res; 301 | 302 | res = ldap_start_tls(ld->ld, NULL, NULL, &msgid); 303 | 304 | info.GetReturnValue().Set(msgid); 305 | } 306 | 307 | void LDAPCnx::InstallTLS(const Nan::FunctionCallbackInfo& info) { 308 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 309 | 310 | info.GetReturnValue().Set(ldap_install_tls(ld->ld)); 311 | } 312 | 313 | void LDAPCnx::CheckTLS(const Nan::FunctionCallbackInfo& info) { 314 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 315 | 316 | info.GetReturnValue().Set(ldap_tls_inplace(ld->ld)); 317 | } 318 | 319 | void LDAPCnx::Abandon(const Nan::FunctionCallbackInfo& info) { 320 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 321 | 322 | info.GetReturnValue().Set(ldap_abandon(ld->ld, info[0]->NumberValue())); 323 | } 324 | 325 | void LDAPCnx::GetErrNo(const Nan::FunctionCallbackInfo& info) { 326 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 327 | int err; 328 | ldap_get_option(ld->ld, LDAP_OPT_RESULT_CODE, &err); 329 | info.GetReturnValue().Set(err); 330 | } 331 | 332 | void LDAPCnx::GetFD(const Nan::FunctionCallbackInfo& info) { 333 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 334 | int fd; 335 | ldap_get_option(ld->ld, LDAP_OPT_DESC, &fd); 336 | info.GetReturnValue().Set(fd); 337 | } 338 | 339 | void LDAPCnx::Delete(const Nan::FunctionCallbackInfo& info) { 340 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 341 | Nan::Utf8String dn(info[0]); 342 | 343 | info.GetReturnValue().Set(ldap_delete(ld->ld, *dn)); 344 | } 345 | 346 | void LDAPCnx::Bind(const Nan::FunctionCallbackInfo& info) { 347 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 348 | Nan::Utf8String dn(info[0]); 349 | Nan::Utf8String pw(info[1]); 350 | 351 | info.GetReturnValue().Set(ldap_simple_bind(ld->ld, 352 | info[0]->IsUndefined()?NULL:*dn, 353 | info[1]->IsUndefined()?NULL:*pw)); 354 | } 355 | 356 | void LDAPCnx::Rename(const Nan::FunctionCallbackInfo& info) { 357 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 358 | Nan::Utf8String dn(info[0]); 359 | Nan::Utf8String newrdn(info[1]); 360 | int res; 361 | 362 | ldap_rename(ld->ld, *dn, *newrdn, NULL, 1, NULL, NULL, &res); 363 | 364 | info.GetReturnValue().Set(res); 365 | } 366 | 367 | void LDAPCnx::Search(const Nan::FunctionCallbackInfo& info) { 368 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 369 | Nan::Utf8String base(info[0]); 370 | Nan::Utf8String filter(info[1]); 371 | Nan::Utf8String attrs(info[2]); 372 | int scope = info[3]->NumberValue(); 373 | int pagesize = info[4]->NumberValue();; 374 | LDAPCookie* cookie = NULL; 375 | 376 | int msgid = 0; 377 | char * attrlist[255]; 378 | 379 | char *bufhead = strdup(*attrs); 380 | char *buf = bufhead; 381 | char **ap; 382 | for (ap = attrlist; (*ap = strsep(&buf, " \t,")) != NULL;) 383 | if (**ap != '\0') 384 | if (++ap >= &attrlist[255]) 385 | break; 386 | 387 | LDAPControl* page_control[2]; 388 | page_control[0] = NULL; 389 | page_control[1] = NULL; 390 | if (pagesize > 0) { 391 | if (info[5]->IsObject() && !info[5]->ToObject().IsEmpty()) 392 | cookie = Nan::ObjectWrap::Unwrap(info[5]->ToObject()); 393 | if (cookie) { 394 | ldap_create_page_control(ld->ld, pagesize, cookie->GetCookie(), 0, &page_control[0]); 395 | } else { 396 | ldap_create_page_control(ld->ld, pagesize, NULL, 0, &page_control[0]); 397 | } 398 | } 399 | 400 | ldap_search_ext(ld->ld, *base, scope, *filter , (char **)attrlist, 0, 401 | page_control, NULL, NULL, 0, &msgid); 402 | if (pagesize > 0) { 403 | ldap_control_free(page_control[0]); 404 | } 405 | 406 | free(bufhead); 407 | 408 | info.GetReturnValue().Set(msgid); 409 | } 410 | 411 | void LDAPCnx::Modify(const Nan::FunctionCallbackInfo& info) { 412 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 413 | Nan::Utf8String dn(info[0]); 414 | 415 | Handle mods = Handle::Cast(info[1]); 416 | unsigned int nummods = mods->Length(); 417 | 418 | LDAPMod **ldapmods = (LDAPMod **) malloc(sizeof(LDAPMod *) * (nummods + 1)); 419 | 420 | for (unsigned int i = 0; i < nummods; i++) { 421 | Local modHandle = 422 | Local::Cast(mods->Get(Nan::New(i))); 423 | 424 | ldapmods[i] = (LDAPMod *) malloc(sizeof(LDAPMod)); 425 | 426 | String::Utf8Value mod_op(modHandle->Get(Nan::New("op").ToLocalChecked())); 427 | 428 | if (!strcmp(*mod_op, "add")) { 429 | ldapmods[i]->mod_op = LDAP_MOD_ADD; 430 | } else if (!strcmp(*mod_op, "delete")) { 431 | ldapmods[i]->mod_op = LDAP_MOD_DELETE; 432 | } else { 433 | ldapmods[i]->mod_op = LDAP_MOD_REPLACE; 434 | } 435 | 436 | String::Utf8Value mod_type(modHandle->Get(Nan::New("attr").ToLocalChecked())); 437 | ldapmods[i]->mod_type = strdup(*mod_type); 438 | 439 | Local modValsHandle = 440 | Local::Cast(modHandle->Get(Nan::New("vals").ToLocalChecked())); 441 | 442 | int modValsLength = modValsHandle->Length(); 443 | ldapmods[i]->mod_values = (char **) malloc(sizeof(char *) * 444 | (modValsLength + 1)); 445 | for (int j = 0; j < modValsLength; j++) { 446 | Nan::Utf8String modValue(modValsHandle->Get(Nan::New(j))); 447 | ldapmods[i]->mod_values[j] = strdup(*modValue); 448 | } 449 | ldapmods[i]->mod_values[modValsLength] = NULL; 450 | } 451 | ldapmods[nummods] = NULL; 452 | 453 | int msgid = ldap_modify(ld->ld, *dn, ldapmods); 454 | 455 | ldap_mods_free(ldapmods, 1); 456 | 457 | info.GetReturnValue().Set(msgid); 458 | } 459 | 460 | void LDAPCnx::Add(const Nan::FunctionCallbackInfo& info) { 461 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 462 | Nan::Utf8String dn(info[0]); 463 | Handle attrs = Handle::Cast(info[1]); 464 | unsigned int numattrs = attrs->Length(); 465 | 466 | LDAPMod **ldapmods = (LDAPMod **) malloc(sizeof(LDAPMod *) * (numattrs + 1)); 467 | for (unsigned int i = 0; i < numattrs; i++) { 468 | Local attrHandle = 469 | Local::Cast(attrs->Get(Nan::New(i))); 470 | 471 | ldapmods[i] = (LDAPMod *) malloc(sizeof(LDAPMod)); 472 | 473 | // Step 1: mod_op 474 | ldapmods[i]->mod_op = LDAP_MOD_ADD; 475 | 476 | // Step 2: mod_type 477 | String::Utf8Value mod_type(attrHandle->Get(Nan::New("attr").ToLocalChecked())); 478 | ldapmods[i]->mod_type = strdup(*mod_type); 479 | 480 | // Step 3: mod_vals 481 | Local attrValsHandle = 482 | Local::Cast(attrHandle->Get(Nan::New("vals").ToLocalChecked())); 483 | int attrValsLength = attrValsHandle->Length(); 484 | ldapmods[i]->mod_values = (char **) malloc(sizeof(char *) * 485 | (attrValsLength + 1)); 486 | for (int j = 0; j < attrValsLength; j++) { 487 | // TODO: handle Buffers here. 488 | Nan::Utf8String modValue(attrValsHandle->Get(Nan::New(j))); 489 | ldapmods[i]->mod_values[j] = strdup(*modValue); 490 | } 491 | ldapmods[i]->mod_values[attrValsLength] = NULL; 492 | } 493 | 494 | ldapmods[numattrs] = NULL; 495 | 496 | int msgid = ldap_add(ld->ld, *dn, ldapmods); 497 | 498 | info.GetReturnValue().Set(msgid); 499 | 500 | ldap_mods_free(ldapmods, 1); 501 | } 502 | 503 | // Attributes matching this list will be returned as Buffer()s 504 | 505 | int LDAPCnx::isBinary(char * attrname) { 506 | if (!strcmp(attrname, "jpegPhoto") || 507 | !strcmp(attrname, "photo") || 508 | !strcmp(attrname, "personalSignature") || 509 | !strcmp(attrname, "userCertificate") || 510 | !strcmp(attrname, "cACertificate") || 511 | !strcmp(attrname, "authorityRevocationList") || 512 | !strcmp(attrname, "certificateRevocationList") || 513 | !strcmp(attrname, "deltaRevocationList") || 514 | !strcmp(attrname, "crossCertificatePair") || 515 | !strcmp(attrname, "x500UniqueIdentifier") || 516 | !strcmp(attrname, "audio") || 517 | !strcmp(attrname, "javaSerializedObject") || 518 | !strcmp(attrname, "thumbnailPhoto") || 519 | !strcmp(attrname, "thumbnailLogo") || 520 | !strcmp(attrname, "supportedAlgorithms") || 521 | !strcmp(attrname, "protocolInformation") || 522 | !strcmp(attrname, "objectGUID") || 523 | !strcmp(attrname, "objectSid") || 524 | strstr(attrname, ";binary")) { 525 | return 1; 526 | } 527 | return 0; 528 | } 529 | -------------------------------------------------------------------------------- /LDAPCnx.h: -------------------------------------------------------------------------------- 1 | #ifndef LDAPCNX_H 2 | #define LDAPCNX_H 3 | 4 | #include 5 | #include 6 | 7 | class LDAPCnx : public Nan::ObjectWrap { 8 | public: 9 | static void Init(v8::Local exports); 10 | Nan::Callback * callback; 11 | Nan::Callback * reconnect_callback; 12 | Nan::Callback * disconnect_callback; 13 | 14 | private: 15 | explicit LDAPCnx(); 16 | ~LDAPCnx(); 17 | 18 | static void New (const Nan::FunctionCallbackInfo& info); 19 | static void Event (uv_poll_t* handle, int status, int events); 20 | static int OnConnect (LDAP *ld, Sockbuf *sb, LDAPURLDesc *srv, 21 | struct sockaddr *addr, struct ldap_conncb *ctx); 22 | static void OnDisconnect(LDAP *ld, Sockbuf *sb, struct ldap_conncb *ctx); 23 | static int OnRebind (LDAP *ld, LDAP_CONST char *url, ber_tag_t request, 24 | ber_int_t msgid, void *params ); 25 | static void Search (const Nan::FunctionCallbackInfo& info); 26 | static void Delete (const Nan::FunctionCallbackInfo& info); 27 | static void Bind (const Nan::FunctionCallbackInfo& info); 28 | static void SASLBind (const Nan::FunctionCallbackInfo& info); 29 | static void Add (const Nan::FunctionCallbackInfo& info); 30 | static void Modify (const Nan::FunctionCallbackInfo& info); 31 | static void Rename (const Nan::FunctionCallbackInfo& info); 32 | static void Abandon (const Nan::FunctionCallbackInfo& info); 33 | static void GetErr (const Nan::FunctionCallbackInfo& info); 34 | static void Close (const Nan::FunctionCallbackInfo& info); 35 | static void GetErrNo (const Nan::FunctionCallbackInfo& info); 36 | static void GetFD (const Nan::FunctionCallbackInfo& info); 37 | static void StartTLS (const Nan::FunctionCallbackInfo& info); 38 | static void InstallTLS (const Nan::FunctionCallbackInfo& info); 39 | static void CheckTLS (const Nan::FunctionCallbackInfo& info); 40 | static int isBinary (char * attrname); 41 | 42 | int SASLBindNext(LDAPMessage** result); 43 | const char* sasl_mechanism; 44 | 45 | ldap_conncb * ldap_callback; 46 | uv_poll_t * handle; 47 | 48 | static Nan::Persistent constructor; 49 | LDAP * ld; 50 | }; 51 | 52 | #endif 53 | 54 | -------------------------------------------------------------------------------- /LDAPCookie.cc: -------------------------------------------------------------------------------- 1 | #include "LDAPCookie.h" 2 | 3 | #include 4 | 5 | Nan::Persistent LDAPCookie::constructor; 6 | 7 | void LDAPCookie::New(const Nan::FunctionCallbackInfo& info) { 8 | LDAPCookie* obj = new LDAPCookie(); 9 | obj->Wrap(info.This()); 10 | 11 | info.GetReturnValue().Set(info.This()); 12 | } 13 | 14 | LDAPCookie::~LDAPCookie() { 15 | if (val_) { 16 | fprintf(stderr, "cookie cleanup"); 17 | ber_bvfree(val_); 18 | } 19 | } 20 | 21 | void LDAPCookie::Init(v8::Local exports) { 22 | Nan::HandleScope scope; 23 | v8::Local tpl = Nan::New(New); 24 | // legal? No idea, just an attempt to prevent polluting javascript global namespace with 25 | // something that doesn't make sense to construct from JS side. Appears to work (doesn't 26 | // crash, paging works). 27 | // tpl->SetClassName(Nan::New("LDAPInternalCookie").ToLocalChecked()); 28 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 29 | constructor.Reset(tpl->GetFunction()); 30 | } 31 | 32 | v8::Local LDAPCookie::NewInstance() { 33 | Nan::EscapableHandleScope scope; 34 | 35 | const unsigned argc = 1; 36 | v8::Local argv[argc] = { Nan::Undefined() }; 37 | v8::Local cons = Nan::New(constructor); 38 | v8::Local instance = Nan::NewInstance(cons, argc, argv) 39 | .ToLocalChecked(); 40 | 41 | return scope.Escape(instance); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /LDAPCookie.h: -------------------------------------------------------------------------------- 1 | #ifndef LDAPCOOKIE_H 2 | #define LDAPCOOKIE_H 3 | 4 | #include 5 | 6 | class LDAPCookie : public Nan::ObjectWrap { 7 | public: 8 | static void Init(v8::Local exports); 9 | static v8::Local NewInstance(); 10 | 11 | void SetCookie(struct berval* cookie) { val_ = cookie; } 12 | struct berval* GetCookie() const { return val_; } 13 | 14 | private: 15 | static Nan::Persistent constructor; 16 | 17 | LDAPCookie() {}; 18 | ~LDAPCookie(); 19 | 20 | static void New(const Nan::FunctionCallbackInfo& info); 21 | 22 | struct berval* val_; 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /LDAPError.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = function LDAPError(message) { 6 | Error.captureStackTrace(this, this.constructor); 7 | this.name = this.constructor.name; 8 | this.message = message; 9 | }; 10 | 11 | require('util').inherits(module.exports, Error); 12 | -------------------------------------------------------------------------------- /LDAPSASL.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "LDAPCnx.h" 3 | #include "SASLDefaults.h" 4 | 5 | using namespace v8; 6 | 7 | void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { 8 | 9 | LDAPCnx* ld = ObjectWrap::Unwrap(info.Holder()); 10 | 11 | if (ld->ld == NULL) { 12 | Nan::ThrowError("LDAP connection has not been established"); 13 | } 14 | 15 | v8::String::Utf8Value mechanism(SASLDefaults::Get(info[0])); 16 | SASLDefaults defaults(info[1], info[2], info[3], info[4]); 17 | v8::String::Utf8Value sec_props(SASLDefaults::Get(info[5])); 18 | 19 | if(*sec_props) { 20 | int res = ldap_set_option(ld->ld, LDAP_OPT_X_SASL_SECPROPS, *sec_props); 21 | if(res != LDAP_SUCCESS) { 22 | Nan::ThrowError(ldap_err2string(res)); 23 | } 24 | } 25 | 26 | int msgid; 27 | LDAPControl** sctrlsp = NULL; 28 | LDAPMessage* message = NULL; 29 | ld->sasl_mechanism = NULL; 30 | 31 | int res = ldap_sasl_interactive_bind(ld->ld, NULL, *mechanism, 32 | sctrlsp, NULL, LDAP_SASL_QUIET, &SASLDefaults::Callback, &defaults, 33 | message, &ld->sasl_mechanism, &msgid); 34 | if(res != LDAP_SASL_BIND_IN_PROGRESS && res != LDAP_SUCCESS) { 35 | Nan::ThrowError(ldap_err2string(res)); 36 | } 37 | 38 | info.GetReturnValue().Set(msgid); 39 | } 40 | 41 | int LDAPCnx::SASLBindNext(LDAPMessage** message) { 42 | LDAPControl** sctrlsp = NULL; 43 | int res; 44 | int msgid; 45 | while(true) { 46 | res = ldap_sasl_interactive_bind(ld, NULL, NULL, 47 | sctrlsp, NULL, LDAP_SASL_QUIET, NULL, NULL, 48 | *message, &sasl_mechanism, &msgid); 49 | 50 | if(res != LDAP_SASL_BIND_IN_PROGRESS) { 51 | break; 52 | } 53 | 54 | ldap_msgfree(*message); 55 | 56 | if(ldap_result(ld, msgid, LDAP_MSG_ALL, NULL, message) == -1) { 57 | ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &res); 58 | break; 59 | } 60 | } 61 | 62 | return res; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /LDAPXSASL.cc: -------------------------------------------------------------------------------- 1 | #include "LDAPCnx.h" 2 | 3 | void LDAPCnx::SASLBind(const Nan::FunctionCallbackInfo& info) { 4 | Nan::ThrowError("LDAP module was not built with SASL support"); 5 | } 6 | 7 | int LDAPCnx::SASLBindNext(LDAPMessage** result) { 8 | return -1; 9 | } 10 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (C) 2010 SSi Micro, Ltd. and other contributors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ldap-client 3.X.X 2 | =============== 3 | 4 | OpenLDAP client bindings for Node.js. Requires libraries from 5 | http://www.openldap.org installed. 6 | 7 | Now uses Nan to ensure it will build for all version of Node.js. 8 | 9 | ***3.X is an API-breaking release***, but it should be easy to convert to the new API. 10 | 11 | NOTE: The module has been renamed to `ldap-client` as `npm` no longer accepts capital letters. 12 | 13 | Contributing 14 | === 15 | 16 | Any and all patches and pull requests are certainly welcome. 17 | 18 | Thanks to: 19 | === 20 | * Petr Běhan 21 | * YANG Xudong 22 | * Victor Powell 23 | * Many other contributors 24 | 25 | Dependencies 26 | === 27 | 28 | Node >= 0.8 29 | 30 | Install 31 | ======= 32 | 33 | You must ensure you have the latest OpenLDAP client libraries 34 | installed from http://www.openldap.org 35 | 36 | To install the latest release from npm: 37 | 38 | npm install --save ldap-client 39 | 40 | You will also require the LDAP Development Libraries (on Ubuntu, `sudo apt-get install libldap2-dev`) 41 | 42 | For SASL authentication support the Cyrus SASL libraries need to be installed 43 | and OpenLDAP needs to be built with SASL support. 44 | 45 | Reconnection 46 | ========== 47 | 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. 48 | 49 | You must close() the instance to stop the reconnect behavior. 50 | 51 | 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. 52 | 53 | API 54 | === 55 | 56 | new LDAP(options, readyCallback); 57 | 58 | Options are provided as a JS object: 59 | 60 | ```js 61 | var LDAP = require('ldap-client'); 62 | 63 | var ldap = new LDAP({ 64 | uri: 'ldap://server', // string 65 | validatecert: false, // Verify server certificate 66 | connecttimeout: -1, // seconds, default is -1 (infinite timeout), connect timeout 67 | base: 'dc=com', // default base for all future searches 68 | attrs: '*', // default attribute list for future searches 69 | filter: '(objectClass=*)', // default filter for all future searches 70 | scope: LDAP.SUBTREE, // default scope for all future searches 71 | connect: function(), // optional function to call when connect/reconnect occurs 72 | disconnect: function(), // optional function to call when disconnect occurs 73 | }, function(err) { 74 | // connected and ready 75 | }); 76 | 77 | ``` 78 | 79 | 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. 80 | 81 | ```js 82 | var ldap = new LDAP({ 83 | uri: 'ldap://server', 84 | connect: function() { 85 | this.bind({ 86 | binddn: 'cn=admin,dc=com', 87 | password: 'supersecret' 88 | }, function(err) { 89 | ... 90 | }); 91 | } 92 | } 93 | ``` 94 | 95 | TLS 96 | === 97 | 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: 98 | 99 | ```js 100 | var LDAP=require('ldap-client'); 101 | 102 | LDAP.LDAP_OPT_X_TLS_NEVER = 0; 103 | LDAP.LDAP_OPT_X_TLS_HARD = 1; 104 | LDAP.LDAP_OPT_X_TLS_DEMAND = 2; 105 | LDAP.LDAP_OPT_X_TLS_ALLOW = 3; 106 | LDAP.LDAP_OPT_X_TLS_TRY = 4; 107 | ``` 108 | 109 | ldap.bind() 110 | === 111 | Calling open automatically does an anonymous bind to check to make 112 | sure the connection is actually open. If you call `bind()`, you 113 | will upgrade the existing anonymous bind. 114 | 115 | ldap.bind(bind_options, function(err)); 116 | 117 | Options are binddn and password: 118 | 119 | ```js 120 | bind_options = { 121 | binddn: '', 122 | password: '' 123 | } 124 | ``` 125 | Aliased to `ldap.simplebind()` for backward compatibility. 126 | 127 | 128 | ldap.saslbind() 129 | === 130 | Upgrade the existing anonymous bind to an authenticated bind using SASL. 131 | 132 | ldap.saslbind([bind_options,] function(err)); 133 | 134 | Options are: 135 | 136 | * mechanism - If not provided SASL library will select based on the best 137 | mechanism available on the server. 138 | * user - Authentication user if required by mechanism 139 | * password - Authentication user's password if required by mechanism 140 | * realm - Non-default SASL realm if required by mechanism 141 | * proxyuser - Authorization (proxy) user if supported by mechanism 142 | * securityproperties - Optional SASL security properties 143 | 144 | All parameters are optional. For example a GSSAPI (Kerberos) bind can be 145 | initiated as follows: 146 | 147 | ``` 148 | ldap.saslbind(function(err) { if(err) throw err; }); 149 | ``` 150 | 151 | For details refer to the [SASL documentation](http://cyrusimap.org/docs/cyrus-sasl). 152 | 153 | 154 | ldap.search() 155 | === 156 | ldap.search(search_options, function(err, data)); 157 | 158 | Options are provided as a JS object: 159 | 160 | ```js 161 | search_options = { 162 | base: 'dc=com', 163 | scope: LDAP.SUBTREE, 164 | filter: '(objectClass=*)', 165 | attrs: '*' 166 | } 167 | ``` 168 | 169 | If one omits any of the above options, then sensible defaults will be used. One can also provide search defaults as part of instantiation. 170 | 171 | Scopes are specified as one of the following integers: 172 | 173 | ```js 174 | var LDAP=require('ldap-client'); 175 | 176 | LDAP.BASE = 0; 177 | LDAP.ONELEVEL = 1; 178 | LDAP.SUBTREE = 2; 179 | LDAP.SUBORDINATE = 3; 180 | LDAP.DEFAULT = -1; 181 | ``` 182 | 183 | List of attributes you want is passed as simple string - join their names 184 | with space if you need more ('objectGUID sAMAccountName cname' is example of 185 | valid attrs filter). '\*' is also accepted. 186 | 187 | Results are returned as an array of zero or more objects. Each object 188 | has attributes named after the LDAP attributes in the found 189 | record(s). Each attribute contains an array of values for that 190 | attribute (even if the attribute is single-valued - having to check typeof() 191 | before you can act on /anything/ is a pet peeve of 192 | mine). The exception to this rule is the 'dn' attribute - this is 193 | always a single-valued string. 194 | 195 | Example of search result: 196 | 197 | ```js 198 | [ { gidNumber: [ '2000' ], 199 | objectClass: [ 'posixAccount', 'top', 'account' ], 200 | uidNumber: [ '3214' ], 201 | uid: [ 'fred' ], 202 | homeDirectory: [ '/home/fred' ], 203 | cn: [ 'fred' ], 204 | dn: 'cn=fred,dc=ssimicro,dc=com' } ] 205 | ``` 206 | 207 | Attributes themselves are usually returned as strings. There is a list of known 208 | binary attribute names hardcoded in C++ binding sources. Those are always 209 | returned as Buffers, but the list is incomplete so far. 210 | 211 | Paged Search Results 212 | === 213 | 214 | LDAP servers are usually limited in how many items they are willing to return - 215 | for example 1000 is Microsoft AD LDS default limit. To get around this limit 216 | for larger directories, you have to use paging (as long as the server supports 217 | it, it's an optional feature). To get paged search, add the "pagesize" attribute 218 | to your search request: 219 | 220 | ```js 221 | search_options = { 222 | ..., 223 | pagesize: n 224 | } 225 | ldap.search(search_options, on_data); 226 | 227 | function on_data(err,data,cookie) { 228 | // handle errors, deal with received data and... 229 | if (cookie) { // more data available 230 | search_options.cookie = cookie; 231 | ldap.search(search_options, on_data); 232 | } else { 233 | // search is complete 234 | } 235 | } 236 | ``` 237 | 238 | RootDSE 239 | === 240 | 241 | As of version 1.2.0 you can also read the rootDSE entry of an ldap server. 242 | To do so, simply issue a read request with base set to an empty string: 243 | 244 | ```js 245 | search_options = { 246 | base: '', 247 | scope: Connection.BASE, 248 | attrs: '+' 249 | // ... other options as necessary 250 | } 251 | ``` 252 | 253 | ldap.findandbind() 254 | === 255 | 256 | ldap.findandbind(fb_options, function(err, data)) 257 | 258 | Options are exactly like the search options, with the addition of a 259 | "password" attribute: 260 | 261 | ```js 262 | fb_options = { 263 | base: '', 264 | filter: '', 265 | scope: '', 266 | attrs: '', 267 | password: '' 268 | } 269 | ``` 270 | 271 | Calls the callback with the record it authenticated against as the 272 | `data` argument. 273 | 274 | `findandbind()` does two convenient things: It searches LDAP for 275 | a record that matches your search filter, and if one (and only one) 276 | result is retured, it then uses a second connection with the same 277 | options as the primary connection to attempt to authenticate to 278 | LDAP as the user found in the first step. 279 | 280 | The idea here is to bind your main LDAP instance with an "admin-like" 281 | account that has the permissions to search. Your (hidden) secondary 282 | connection will be used only for authenticating users. 283 | 284 | In contrast, the `bind()` method will, if successful, change the 285 | authentication on the primary connection. 286 | 287 | ```js 288 | ldap.bind({ 289 | binddn: 'cn=admin,dc=com', 290 | password: 'supersecret' 291 | }, function(err, data) { 292 | if (err) { 293 | ... 294 | } 295 | // now we're authenticated as admin on the main connection 296 | // and thus have the correct permissions for search 297 | 298 | ldap.findandbind({ 299 | filter: '(&(username=johndoe)(status=enabled))', 300 | attrs: 'username homeDirectory' 301 | }, function(err, data) { 302 | if (err) { 303 | ... 304 | } 305 | // our main connection is still cn=admin 306 | // but there's a hidden connection bound 307 | // as "johndoe" 308 | console.log(data[0].homeDirectory[0]); 309 | } 310 | } 311 | 312 | ``` 313 | 314 | If you ensure that the "admin" user (or whatever you bind as for 315 | the main connection) can not READ the password field, then 316 | passwords will never leave the LDAP server -- all authentication 317 | is done my the LDAP server itself. 318 | 319 | 320 | ldap.add() 321 | === 322 | 323 | ldap.add(dn, [attrs], function(err)) 324 | 325 | dn is the full DN of the record you want to add, attrs to be provided 326 | as follows: 327 | 328 | ```js 329 | var attrs = [ 330 | { attr: 'objectClass', vals: [ 'organizationalPerson', 'person', 'top' ] }, 331 | { attr: 'sn', vals: [ 'Smith' ] }, 332 | { attr: 'badattr', vals: [ 'Fried' ] } 333 | ] 334 | ``` 335 | 336 | ldap.modify() 337 | === 338 | 339 | ldap.modify(dn, [ changes ], function(err)) 340 | 341 | Modifies the provided dn as per the changes array provided. Ops are 342 | one of "add", "delete" or "replace". 343 | 344 | ```js 345 | var changes = [ 346 | { op: 'add', 347 | attr: 'title', 348 | vals: [ 'King of Callbacks' ] 349 | } 350 | ] 351 | ``` 352 | 353 | ldap.rename() 354 | === 355 | 356 | ldap.rename(dn, newrdn, function(err)) 357 | 358 | Will rename the entry to the new RDN provided. 359 | 360 | Example: 361 | 362 | ```js 363 | ldap.rename('cn=name,dc=example,dc=com', 'cn=newname') 364 | ``` 365 | 366 | ldap.remove() 367 | === 368 | 369 | ldap.remove(dn, function(err)) 370 | 371 | Deletes an entry. 372 | 373 | Example: 374 | 375 | ```js 376 | ldap.remove('cn=name,dc=example,dc=com', function(err) { 377 | if (err) { 378 | // Could not delete entry 379 | } 380 | }); 381 | ``` 382 | 383 | Escaping 384 | === 385 | Yes, Virginia, there's such a thing as LDAP injection attacks. 386 | 387 | There are a few helper functions to ensure you are escaping your input properly. 388 | 389 | **escapefn(type, template)** 390 | Returns a function that escapes the provided parameters and inserts them into the provided template: 391 | 392 | ```js 393 | var LDAP = require('ldap-client'); 394 | var userSearch = LDAP.escapefn('filter', 395 | '(&(objectClass=%s)(cn=%s))'); 396 | 397 | ... 398 | ldap.search({ 399 | filter: userSearch('posixUser', username), 400 | scope: LDAP.SUBTREE 401 | }, function(err, data) { 402 | ... 403 | }); 404 | ``` 405 | Since the escaping rules are different for DNs vs search filters, `type` should be one of `'filter'` or `'dn'`. 406 | 407 | To escape a single string, `LDAP.stringEscapeFilter`: 408 | 409 | ```js 410 | var LDAP=require('ldap-client'); 411 | var user = "John O'Doe"; 412 | 413 | LDAP.stringEscapeFilter('(username=' + user + ')'); 414 | // ==> '(username=John O\'Doe)' 415 | ``` 416 | 417 | 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: 418 | 419 | ```js 420 | var LDAP = require('ldap-client'); 421 | var escapeDN = LDAP.escapefn('dn', 422 | 'cn=%s,dc=sample,dc=com'); 423 | 424 | ... 425 | var safeDN = escapeDN(" O'Doe"); 426 | // => "cn=\ O\'Doe,dc=sample,dc=com" 427 | 428 | ``` 429 | 430 | Bugs 431 | === 432 | Domain errors don't work properly. Domains are deprecated as of node 4, 433 | so I don't think I'm going to track it down. If you need domain handling, 434 | let me know. 435 | 436 | TODO Items 437 | === 438 | Basically, these are features I don't really need myself. 439 | 440 | * Filter escaping 441 | 442 | -------------------------------------------------------------------------------- /SASLDefaults.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "SASLDefaults.h" 3 | 4 | void SASLDefaults::Set(unsigned flags, sasl_interact_t *interact) { 5 | const char *dflt = interact->defresult; 6 | 7 | switch (interact->id) { 8 | case SASL_CB_AUTHNAME: 9 | dflt = *user; 10 | break; 11 | case SASL_CB_PASS: 12 | dflt = *password; 13 | break; 14 | case SASL_CB_GETREALM: 15 | dflt = *realm; 16 | break; 17 | case SASL_CB_USER: 18 | dflt = *proxy_user; 19 | break; 20 | } 21 | 22 | interact->result = (dflt && *dflt) ? dflt : ""; 23 | interact->len = strlen((const char*)interact->result); 24 | } 25 | 26 | int SASLDefaults::Callback(LDAP *ld, unsigned flags, void *defaults, void *in) { 27 | SASLDefaults* self = (SASLDefaults*)defaults; 28 | sasl_interact_t *interact = (sasl_interact_t*)in; 29 | while(interact->id != SASL_CB_LIST_END) { 30 | self->Set(flags, interact); 31 | ++interact; 32 | } 33 | 34 | return LDAP_SUCCESS; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /SASLDefaults.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct SASLDefaults { 5 | SASLDefaults( 6 | const v8::Local& usr, 7 | const v8::Local& pw, 8 | const v8::Local& rlm, 9 | const v8::Local& proxy 10 | ) : 11 | user(Get(usr)), 12 | password(Get(pw)), 13 | realm(Get(rlm)), 14 | proxy_user(Get(proxy)) 15 | {} 16 | 17 | // Returns a C NULL value if not a string 18 | static inline v8::Local Get(const v8::Local& v) { 19 | return v->IsString() ? v : v8::Local(); 20 | } 21 | 22 | static int Callback(LDAP *ld, unsigned flags, void *defaults, void *in); 23 | 24 | v8::String::Utf8Value user; 25 | v8::String::Utf8Value password; 26 | v8::String::Utf8Value realm; 27 | v8::String::Utf8Value proxy_user; 28 | 29 | private: 30 | void Set(unsigned flags, sasl_interact_t *interact); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "LDAPCnx", 5 | "sources": [ "LDAP.cc", "LDAPCnx.cc", "LDAPCookie.cc", 6 | "LDAPSASL.cc", "LDAPXSASL.cc", "SASLDefaults.cc" ], 7 | "include_dirs" : [ 8 | "|=|\\/g), 39 | replacements: { 40 | "\0": "\\00", 41 | " ": "\\ ", 42 | "\"": "\\\"", 43 | "#": "\\#", 44 | "+": "\\+", 45 | ",": "\\,", 46 | ";": "\\;", 47 | "<": "\\<", 48 | ">": "\\>", 49 | "=": "\\=", 50 | "\\": "\\5C" 51 | } 52 | } 53 | }; 54 | 55 | function Stats() { 56 | this.lateresponses = 0; 57 | this.reconnects = 0; 58 | this.timeouts = 0; 59 | this.requests = 0; 60 | this.searches = 0; 61 | this.binds = 0; 62 | this.errors = 0; 63 | this.modifies = 0; 64 | this.adds = 0; 65 | this.removes = 0; 66 | this.renames = 0; 67 | this.disconnects = 0; 68 | this.results = 0; 69 | return this; 70 | } 71 | 72 | function LDAP(opt, fn) { 73 | this.queue = {}; 74 | this.stats = new Stats(); 75 | 76 | this.options = extendobj({ 77 | base: 'dc=com', 78 | filter: '(objectClass=*)', 79 | scope: 2, 80 | attrs: '*', 81 | ntimeout: 1000, 82 | timeout: 2000, 83 | debug: 0, 84 | validatecert: LDAP.LDAP_OPT_X_TLS_HARD, 85 | referrals: 0, 86 | connect: function() {}, 87 | disconnect: function() {} 88 | }, opt); 89 | 90 | if (typeof this.options.uri === 'string') { 91 | this.options.uri = [ this.options.uri ]; 92 | } 93 | 94 | this.ld = new binding.LDAPCnx(this.dequeue.bind(this), 95 | this.onconnect.bind(this), 96 | this.ondisconnect.bind(this), 97 | this.options.uri.join(' '), 98 | this.options.ntimeout, 99 | this.options.debug, 100 | this.options.validatecert, 101 | this.options.referrals); 102 | 103 | if (typeof fn !== 'function') { 104 | fn = function() {}; 105 | } 106 | 107 | return this.enqueue(this.ld.bind(undefined, undefined), fn); 108 | } 109 | 110 | LDAP.prototype.onconnect = function() { 111 | this.stats.reconnects++; 112 | return this.options.connect.call(this); 113 | }; 114 | 115 | LDAP.prototype.ondisconnect = function() { 116 | this.stats.disconnects++; 117 | this.options.disconnect(); 118 | }; 119 | 120 | LDAP.prototype.starttls = function(fn) { 121 | return this.enqueue(this.ld.starttls(), fn); 122 | }; 123 | 124 | LDAP.prototype.installtls = function() { 125 | return this.ld.installtls(); 126 | }; 127 | 128 | LDAP.prototype.tlsactive = function() { 129 | return this.ld.checktls(); 130 | }; 131 | 132 | LDAP.prototype.remove = LDAP.prototype.delete = function(dn, fn) { 133 | this.stats.removes++; 134 | if (typeof dn !== 'string' || 135 | typeof fn !== 'function') { 136 | throw new LDAPError('Missing argument'); 137 | } 138 | return this.enqueue(this.ld.delete(dn), fn); 139 | }; 140 | 141 | LDAP.prototype.bind = LDAP.prototype.simplebind = function(opt, fn) { 142 | this.stats.binds++; 143 | if (typeof opt === 'undefined' || 144 | typeof opt.binddn !== 'string' || 145 | typeof opt.password !== 'string' || 146 | typeof fn !== 'function') { 147 | throw new LDAPError('Missing argument'); 148 | } 149 | return this.enqueue(this.ld.bind(opt.binddn, opt.password), fn); 150 | }; 151 | 152 | LDAP.prototype.saslbind = function(opt, fn) { 153 | 154 | this.stats.binds++; 155 | 156 | if(arguments.length == 1 && typeof arguments[0] === 'function') { 157 | fn = opt; 158 | opt = undefined; 159 | } 160 | 161 | var args = [ 162 | 'mechanism','user','password','realm','proxyuser','securityproperties' 163 | ].map(function(p) { return opt == null ? undefined : opt[p]; }); 164 | 165 | if (args.filter(function(a) { 166 | return a != null && typeof a !== 'string'; 167 | }).length || 168 | typeof fn !== 'function') { 169 | throw new LDAPError('Invalid argument'); 170 | } 171 | 172 | return this.enqueue(this.ld.saslbind.apply(this.ld, args), fn); 173 | }; 174 | 175 | LDAP.prototype.add = function(dn, attrs, fn) { 176 | this.stats.adds++; 177 | if (typeof dn !== 'string' || 178 | typeof attrs !== 'object') { 179 | throw new LDAPError('Missing argument'); 180 | } 181 | return this.enqueue(this.ld.add(dn, attrs), fn); 182 | }; 183 | 184 | LDAP.prototype.search = function(opt, fn) { 185 | this.stats.searches++; 186 | return this.enqueue(this.ld.search(arg(opt.base , this.options.base), 187 | arg(opt.filter , this.options.filter), 188 | arg(opt.attrs , this.options.attrs), 189 | arg(opt.scope , this.options.scope), 190 | arg(opt.pagesize, this.options.pagesize), 191 | arg(opt.cookie, null) 192 | ), unwrap_cookie); 193 | function unwrap_cookie(err, data) { 194 | err ? fn(err) : fn(err, data.data, data.cookie); 195 | } 196 | }; 197 | 198 | LDAP.prototype.rename = function(dn, newrdn, fn) { 199 | this.stats.renames++; 200 | if (typeof dn !== 'string' || 201 | typeof newrdn !== 'string' || 202 | typeof fn !== 'function') { 203 | throw new LDAPError('Missing argument'); 204 | } 205 | return this.enqueue(this.ld.rename(dn, newrdn), fn); 206 | }; 207 | 208 | LDAP.prototype.modify = function(dn, ops, fn) { 209 | this.stats.modifies++; 210 | if (typeof dn !== 'string' || 211 | typeof ops !== 'object' || 212 | typeof fn !== 'function') { 213 | throw new LDAPError('Missing argument'); 214 | } 215 | return this.enqueue(this.ld.modify(dn, ops), fn); 216 | }; 217 | 218 | LDAP.prototype.findandbind = function(opt, fn) { 219 | if (opt === undefined || 220 | opt.password === undefined) { 221 | throw new Error('Missing argument'); 222 | } 223 | 224 | this.search(opt, function findandbindFind(err, data) { 225 | if (err) return fn(err); 226 | 227 | if (data === undefined || data.length != 1) { 228 | return fn(new LDAPError('Search returned ' + data.length + ' results, expected 1')); 229 | } 230 | if (this.auth_connection === undefined) { 231 | this.auth_connection = new LDAP(this.options, function newAuthConnection(err) { 232 | if (err) return fn(err); 233 | return this.authbind(data[0].dn, opt.password, function authbindResult(err) { 234 | fn(err, data[0]); 235 | }); 236 | }.bind(this)); 237 | } else { 238 | this.authbind(data[0].dn, opt.password, function authbindResult(err) { 239 | fn(err, data[0]); 240 | }); 241 | } 242 | return undefined; 243 | }.bind(this)); 244 | }; 245 | 246 | LDAP.prototype.authbind = function(dn, password, fn) { 247 | this.auth_connection.bind({ binddn: dn, password: password }, fn.bind(this)); 248 | }; 249 | 250 | LDAP.prototype.close = function() { 251 | if (this.auth_connection !== undefined) { 252 | this.auth_connection.close(); 253 | } 254 | this.ld.close(); 255 | this.ld = undefined; 256 | }; 257 | 258 | LDAP.prototype.dequeue = function(err, msgid, data) { 259 | this.stats.results++; 260 | if (this.queue[msgid]) { 261 | clearTimeout(this.queue[msgid].timer); 262 | this.queue[msgid](err, data); 263 | delete this.queue[msgid]; 264 | } else { 265 | this.stats.lateresponses++; 266 | } 267 | }; 268 | 269 | LDAP.prototype.enqueue = function(msgid, fn) { 270 | if (msgid == -1 || this.ld === undefined) { 271 | if (this.ld.errorstring() === 'Can\'t contact LDAP server') { 272 | // this means we have had a disconnect event, but since there 273 | // are still requests outstanding from libldap's perspective, 274 | // the connection isn't "closed" and the disconnect event has 275 | // not yet fired. To get libldap to actually call the disconnect 276 | // handler, we need to dump all outstanding requests, and hope 277 | // we're not missing one for some reason. Only once we've 278 | // abandoned everything does the handle properly close. 279 | Object.keys(this.queue).forEach(function fireTimeout(msgid) { 280 | this.queue[msgid](new LDAPError('Timeout')); 281 | delete this.queue[msgid]; 282 | this.ld.abandon(msgid); 283 | }.bind(this)); 284 | } 285 | process.nextTick(function emitError() { 286 | fn(new LDAPError(this.ld.errorstring())); 287 | }.bind(this)); 288 | this.stats.errors++; 289 | return this; 290 | } 291 | fn.timer = setTimeout(function searchTimeout() { 292 | this.ld.abandon(msgid); 293 | delete this.queue[msgid]; 294 | fn(new LDAPError('Timeout')); 295 | this.stats.timeouts++; 296 | }.bind(this), this.options.timeout); 297 | this.queue[msgid] = fn; 298 | this.stats.requests++; 299 | return this; 300 | }; 301 | 302 | function stringescape(escapes_obj, str) { 303 | return str.replace(escapes_obj.regex, function (match) { 304 | return escapes_obj.replacements[match]; 305 | }); 306 | } 307 | 308 | LDAP.escapefn = function(type, template) { 309 | var escapes_obj = escapes[type]; 310 | return function() { 311 | var args = [ template ], i; 312 | for (i = 0 ; i < arguments.length ; i++) { // optimizer-friendly 313 | args.push(stringescape(escapes_obj, arguments[i])); 314 | } 315 | return util.format.apply(this,args); 316 | }; 317 | }; 318 | 319 | LDAP.stringEscapeFilter = LDAP.escapefn('filter', '%s'); 320 | 321 | function setConst(target, name, val) { 322 | target.prototype[name] = target[name] = val; 323 | } 324 | 325 | setConst(LDAP, 'BASE', 0); 326 | setConst(LDAP, 'ONELEVEL', 1); 327 | setConst(LDAP, 'SUBTREE', 2); 328 | setConst(LDAP, 'SUBORDINATE', 3); 329 | setConst(LDAP, 'DEFAULT', 4); 330 | 331 | setConst(LDAP, 'LDAP_OPT_X_TLS_NEVER', 0); 332 | setConst(LDAP, 'LDAP_OPT_X_TLS_HARD', 1); 333 | setConst(LDAP, 'LDAP_OPT_X_TLS_DEMAND', 2); 334 | setConst(LDAP, 'LDAP_OPT_X_TLS_ALLOW', 3); 335 | setConst(LDAP, 'LDAP_OPT_X_TLS_TRY', 4); 336 | 337 | module.exports = LDAP; 338 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Jeremy Childs ", 3 | "name": "ldap-client", 4 | "description": "LDAP Binding for node.js", 5 | "homepage": "https://github.com/jeremycx/node-LDAP", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/jeremycx/node-LDAP.git" 9 | }, 10 | "main": "index.js", 11 | "license": "MIT", 12 | "scripts": { 13 | "configure": "node-gyp configure", 14 | "build": "node-gyp rebuild", 15 | "test": "mocha" 16 | }, 17 | "devDependencies": { 18 | "jshint": "^2.8.0", 19 | "mocha": "^2.2.5" 20 | }, 21 | "dependencies": { 22 | "bindings": "^1.2.1", 23 | "nan": "^2.12.1", 24 | "node-gyp": "" 25 | }, 26 | "engines": { 27 | "node": ">= 0.8.0" 28 | }, 29 | "version": "3.1.3" 30 | } 31 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /openldap-data 2 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Testing this module requires the OpenLDAP slapd to be installed, and 5 | in the $PATH. Executing run_tests.sh will launch slapd on port 1234, 6 | and add some basic default values to it. 7 | -------------------------------------------------------------------------------- /test/add.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=Edward,ou=Accounting,dc=sample,dc=com 2 | objectClass: organizationalPerson 3 | objectClass: person 4 | objectClass: top 5 | cn: Edward 6 | sn: Root 7 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 8 | 9 | dn: cn=Fred,ou=Accounting,dc=sample,dc=com 10 | objectClass: organizationalPerson 11 | objectClass: person 12 | objectClass: top 13 | cn: Fred 14 | sn: Root 15 | userPassword: ajsh 16 | 17 | dn: cn=Gary,ou=Accounting,dc=sample,dc=com 18 | objectClass: organizationalPerson 19 | objectClass: person 20 | objectClass: top 21 | cn: Gary 22 | sn: Root 23 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 24 | 25 | dn: cn=Hank,ou=Accounting,dc=sample,dc=com 26 | objectClass: organizationalPerson 27 | objectClass: person 28 | objectClass: top 29 | cn: Hank 30 | sn: Root 31 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 32 | -------------------------------------------------------------------------------- /test/add2.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=Ian,ou=Accounting,dc=sample,dc=com 2 | objectClass: organizationalPerson 3 | objectClass: person 4 | objectClass: top 5 | cn: Ian 6 | sn: Root 7 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 8 | -------------------------------------------------------------------------------- /test/certs/._.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycx/node-LDAP/65029c70c6140de0460b3c3df403ce3fc5d196fa/test/certs/._.DS_Store -------------------------------------------------------------------------------- /test/certs/device.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnDCCAoQCCQDpkoraKTv49zANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC 3 | Q0ExCzAJBgNVBAgTAk5UMRQwEgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChME 4 | RGVtbzENMAsGA1UECxMERGVtbzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20x 5 | IzAhBgkqhkiG9w0BCQEWFGplcmVteWNAc3NpbWljcm8uY29tMB4XDTE1MTAyMjE1 6 | NTcxOFoXDTI5MDYzMDE1NTcxOFowgY8xCzAJBgNVBAYTAkNBMQswCQYDVQQIEwJO 7 | VDEUMBIGA1UEBxMLWWVsbG93a25pZmUxDTALBgNVBAoTBERlbW8xDTALBgNVBAsT 8 | BERlbW8xGjAYBgNVBAMTEWRlbW8uc3NpbWljcm8uY29tMSMwIQYJKoZIhvcNAQkB 9 | FhRqZXJlbXljQHNzaW1pY3JvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 10 | AQoCggEBAMBx7klo3CxR5vvFxf0Su58PXQjFe5IEc3p0HKXsOHNVOchIy1raU0+O 11 | RpBFX+e/XkNPjMi/0Y4TKLiwxKVW7KtBBltBRx+2UjuY4qIWAZJQSGcq6qNAtzms 12 | tQP2HWOhSeFFHoW1NXK88HYo7KDVIAD135cUSvn5+jqiwGYe0rX/lBUkOCmPQu6/ 13 | LyzBDgRVsrZOUzGdgsWjhQQFQSPM6LlgOzCkj1oCGgaO8C7/9D1p+f2ACP5zTcE+ 14 | JZ3Sn1ry10IK58RBAR0tQnX6o06cSlLzxNbj5/Zl2rA/r0nB8ZN/iILbas440V+h 15 | DPPxo1irBsW9TsElA5JWHi/KXBXfZSsCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA 16 | sd3QR94dPIPpi+EmkD9pKuLu6UTTQXe49QaqdZ1zbmzcm5I446Mnca5QbwrjR1HJ 17 | mLyQ7vUeIqBWwJTmXnKS7A0ZjeSXy1r4mC8oHdyjF/2xgYXPltsaKUjn+qBUo/ID 18 | QgOAREfn+sR3hoqUsHFCohW6mO4ZLartUNRlliNWWATaq60SB5AmMDe9UixSq5xq 19 | 9i073cNmnWUcIJ/ApWh5jS6FlHL7P7tBdWXR4+yud9+18khdeab3HW7diFGTNsvU 20 | XirNk7tjReltkgPqfRcCe9gv0QVgy31aK0eBNvt15IiT3jhQdEC1W3TyvId3MhTa 21 | xNzjR8MXrASMZbIve6tFQw== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test/certs/device.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC1TCCAb0CAQAwgY8xCzAJBgNVBAYTAkNBMQswCQYDVQQIEwJOVDEUMBIGA1UE 3 | BxMLWWVsbG93a25pZmUxDTALBgNVBAoTBERlbW8xDTALBgNVBAsTBERlbW8xGjAY 4 | BgNVBAMTEWRlbW8uc3NpbWljcm8uY29tMSMwIQYJKoZIhvcNAQkBFhRqZXJlbXlj 5 | QHNzaW1pY3JvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMBx 6 | 7klo3CxR5vvFxf0Su58PXQjFe5IEc3p0HKXsOHNVOchIy1raU0+ORpBFX+e/XkNP 7 | jMi/0Y4TKLiwxKVW7KtBBltBRx+2UjuY4qIWAZJQSGcq6qNAtzmstQP2HWOhSeFF 8 | HoW1NXK88HYo7KDVIAD135cUSvn5+jqiwGYe0rX/lBUkOCmPQu6/LyzBDgRVsrZO 9 | UzGdgsWjhQQFQSPM6LlgOzCkj1oCGgaO8C7/9D1p+f2ACP5zTcE+JZ3Sn1ry10IK 10 | 58RBAR0tQnX6o06cSlLzxNbj5/Zl2rA/r0nB8ZN/iILbas440V+hDPPxo1irBsW9 11 | TsElA5JWHi/KXBXfZSsCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4IBAQA8LNE65w+r 12 | zBLxvZ64o2xSDS3QAFox6sEXCDSCe/0ExJ56TzvaGbUET9HnlDrHcOrWEyIVxkf0 13 | Ifyyzz0akpNYcBSfY5cckipmIIBSVcXYVGTDRJ/pdls58Nh+CMXMkR+PQ5dBvNBK 14 | GTh/MVLGTYdpvDw0gEprqi3VevYkEtg2QpLt/AfKiHMOkZ8F5lo+oRF+D/GJmt5r 15 | 2tZDfJVWgoYlkMtRRuJZUOQAp9XFwl+K96/MLh/IlY41RbzQNyG898PRRfslTXB1 16 | dmT56IIuLz47fS7Dxd0XqzpE7QJUeJXKZGwvthZc6C8k2lH23dOWvLqHsaY3VfZL 17 | 36wOVxdY4PR+ 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /test/certs/device.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAwHHuSWjcLFHm+8XF/RK7nw9dCMV7kgRzenQcpew4c1U5yEjL 3 | WtpTT45GkEVf579eQ0+MyL/RjhMouLDEpVbsq0EGW0FHH7ZSO5jiohYBklBIZyrq 4 | o0C3Oay1A/YdY6FJ4UUehbU1crzwdijsoNUgAPXflxRK+fn6OqLAZh7Stf+UFSQ4 5 | KY9C7r8vLMEOBFWytk5TMZ2CxaOFBAVBI8zouWA7MKSPWgIaBo7wLv/0PWn5/YAI 6 | /nNNwT4lndKfWvLXQgrnxEEBHS1CdfqjTpxKUvPE1uPn9mXasD+vScHxk3+Igttq 7 | zjjRX6EM8/GjWKsGxb1OwSUDklYeL8pcFd9lKwIDAQABAoIBAQCSKrazGSsJmpeX 8 | KWsswbqxoCiojd5CVJElM+XCfH2P0+6UWf3inqriZQzhbV/flHFTLKugmlje0Vx/ 9 | kvt5HWGa3UOnshgEVSV2ULPqKk69Q68KdQVMQ84mxy+ht6Aw2QNVT3tUUQMsh6cY 10 | CBNaQSYStK1Dgc1EuoI9YPpDVivywL+2TCUDhSzyTOlmuN71eVJVJ5z5lEVRTcjH 11 | kZhyojJbc4bOVWNtd04E0lINYb47Nw5y42Dbl3hzXEHjbxDtqwaH/8zCr514UKwb 12 | R0sP2qbZGhW3D8SKFobqFKBioO5RIOBaLvAN5IbmgjNNelk3jKVrNireczbRRY7t 13 | 6pGEfi4RAoGBAOj4R414Z5yEM3z5IGctOKnNlnvqV1t8OuAn1admhxOpFQmEpdsy 14 | FgO3dQ2i1wVomWJFnf05nLqnhMs5RInOPt4X5FPuL3O9FQpRfo7la0JGxF0ILWyY 15 | dpIsBhFvBFKX/KcklX+TU/Pvw/6sj0H2vb+KadNrCZo4F06vDgGQM4JJAoGBANN4 16 | GTKR9PQnhg5LAYVFQ27W8cUzMyvhr3t9DhrA/4NQNfPO5NdUSVyzIScO37RjHlB/ 17 | yjRiATGkhz0xWidxef716tVNpSNpH/exBL8UGmTNPwp89Uy/N9mgYo/yVwugcGor 18 | iqxvh2s7kyHVfZffWoEN9Q1I28LbkqejNNB8QuvTAoGAOt7qrew8OogJvs3xi0EZ 19 | LYefPGcGdj7ZXeWTDv9QqP40K7iSdOaeO4gzkyOQNHSvNe8jsmbJnT1RyE0Lbctp 20 | hZQCBdeNtDCWzYm0coW06gWZ/2xeli+c3ukzC1rDe9+eX9pV0Ow47c6r94JBnUit 21 | wGZIwb0tqwP7l82Su4BmE8kCgYAo92MqQMxLYDzAGBe7Uae2mT1NDpYjMh1ktt08 22 | oZbeQXOyP6plbJaptqn9fwwnTexZe+gYLcQ9cbohSKZGbd1MXyeXGuua6Iqg2VIq 23 | EiLq1DgaOArtSz3ukvuFF1V1kycz6it7LD/3rhrauxkRittllOacJDkujorinuNk 24 | YC42sQKBgQDNY2eCtKMC7Lf8Lm5jnbNdW7s3iGSkeHBrxLf6+5JaV9ANHPU2DC23 25 | rUecryszi/mIePeEYbbSiqxSa/2rbIS8s4WkNRpXENWRpACaOeRzNLhxbL5kFKKh 26 | aiiK/+rGS+T1KDSoTm5VYsyj1MG0bRfIdGhrnvCapDgTBDchItF82w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/certs/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,22673A99BDDF38E8 4 | 5 | ffN8zTCH8u/KHRSdxR8KrcXMKkN3laq5e4gANucNBznvj7Nx4mCHi82ggdc/KRu9 6 | lSS90qb7Mkcd9bJZ9WiJF+JnaShAGlLH0vyfwYN1EniSCx6HlqPCGgBt/w6bU97o 7 | HLPRsWHXIVf/LZ+YcB8X9Z0e/ookZqBHsbsGb2+uBX7EDtok6P6K+wYR1ousppDA 8 | 3cTp42e4U2egNt1bUyYFnfC3+p5Rci7wHopcrDgouEP8NeSqM7Jhrtl59Kl0eHvV 9 | nI2G//5asbvlLz1CcY+HkJ3acJbsiUwxXQtUcLmAytgKiyJ6Mc8tF8e2hpbLmGJZ 10 | y0QY/Tc2eUXAX//nxnlyAT1YGghAORnxyQU6+lXvqk/9qLq/fEaT9GhcK1bVqCxU 11 | vkMKZx9WleetuESgpo83J/RrOdoToQL0ngyd311ivmuFUg7RaJLAPVtxiz3nydxN 12 | 2QXkKaGw4UOnEgkDVGyzIfJLqTuuiluFuvAPx1DhbdIe7schbM5AIbpkEtnJIVU1 13 | QMxZ/P51rjJlBOqELQBEiD63dZ6J7MBX7zzuEOLF8SW1RyuA5y2f8O0n/DubqapH 14 | ZC0uZGfG6C/Auy+DY/wrAfFsgA4DBGUdX3/iXg0SXJcwIwOOyJdUAgWbtNtMx0vu 15 | qOb1vu28UMfns6pPi64DWSARy4pDkpPKsPnakQ7M87sOiaXSM259RAwJhMoWJwcc 16 | xPkWOsF7hKS1Jy/ZVfZcbIF4Fs6m6Zo1oUi35blVH5QfKlLujP8jlaF7UgFovI7w 17 | 1zoJ6JU99ZAFT3gA9GOQYIWEDq+3MCnOmqU+JlNynsrOr7kF9PkoewQuzcJeA0/n 18 | MP1O6dCVmqdBngt1nAHTyXjiKQj5WmFsoaAhzl5daOL0fSTcBnDXBqTjPZl8l3Iy 19 | FN5r7pVYyggWCgHoMQiV0zUUAb80jiLNaHULjhUakeKbVIOTagPDpy36K3xRrFz2 20 | 1cM1XpJKfTaN9Rovf6+BRr6ecqUHVStdgusAW5VErSsYmZhz5KuyYJeZwFnZR7uP 21 | SPCD8QwBsLpf3t9h2UoBe/GTKZcajnNv6nZ/ld5YkPa2G+BMZlg5/wNhhfdc5vjV 22 | czeixVl3iOFn5zwbUCPZq1oxXkgT5HExwWGqKtAUyjg2O4tWUDshJX7vwlQ8PEJ5 23 | 9Fy61ZWlzY1xTzYIh3AzQAHHSWVqt6cuOXITlTJGCON02OHgJ56NOe/Ci7/XWyoI 24 | k+SQ2dvPjoaQL5r7HS6fmRO+VlcugB3zSBiTZTCv4+z1/cUVT8HWH6PNV2cDAxx7 25 | HCGmTPe3WFJZxKTxGJ6x1NKRsyz6a5Uk3BzB5HVG5av3sXlTDy6dNI5oXHtZ1ND5 26 | 2M2KNC9bif8RqrUXrhbrTLbIYIog6JxJcdJsxxkhTInQs7P5/xEvKxosHkxdWTM7 27 | UVH4c/5WRn35l2jTUe4QIKWokrCDggpbyh+qMLo7AEdUQ72raXZ3MxKD9tXL82xh 28 | uNV+vG0ojsy4zjFBEGqlbchDShfOVzIZBRV2Q7yAKWJ9h6Q2sTl2CQG8obG73b2K 29 | QDJ6jw2H3BeaBXnWdbl3QqhPVPCA4Tl8I2egkujPILUkDLVsLeq95g== 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /test/certs/rootCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEmzCCA4OgAwIBAgIJAL6wGd1s4F9TMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYD 3 | VQQGEwJDQTELMAkGA1UECBMCTlQxFDASBgNVBAcTC1llbGxvd2tuaWZlMQ0wCwYD 4 | VQQKEwREZW1vMQ0wCwYDVQQLEwREZW1vMRowGAYDVQQDExFkZW1vLnNzaW1pY3Jv 5 | LmNvbTEjMCEGCSqGSIb3DQEJARYUamVyZW15Y0Bzc2ltaWNyby5jb20wHhcNMTUx 6 | MDIyMTU1NjAwWhcNMTgwODExMTU1NjAwWjCBjzELMAkGA1UEBhMCQ0ExCzAJBgNV 7 | BAgTAk5UMRQwEgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChMERGVtbzENMAsG 8 | A1UECxMERGVtbzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20xIzAhBgkqhkiG 9 | 9w0BCQEWFGplcmVteWNAc3NpbWljcm8uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC 10 | AQ8AMIIBCgKCAQEA0s3y2/WY1ZEtsU6/5UwRCZKsf88ApctqB3P5aTB9Ow53AOLF 11 | hj/oN/cT2qfFtPAp0jKtUS9/bROQbsy0tzRc9OBDZ5qc7XZhlXikcPAN16esmA7j 12 | uyNdQ6wgfX9GVdOywQKONEqePvg+SX9xiq5TrulDfHF2IS+G1UJRWkACuGSaTXhb 13 | v5CCQceSvPipRZts+7SMERkgciCH2oVuyGs6n7Sc1LGmNtq7FsQgTs8RvtgEJ+eV 14 | SkGqiy4/59evohRg2fSos/kfQFGMvYyYj4EDe8spnGOa919wU5z+16Oog/VOB/jv 15 | V3CpRuegD2R9at1Rc8XGb+xpwn0JtjTbYDEQnQIDAQABo4H3MIH0MB0GA1UdDgQW 16 | BBQr9y+Kq4lHtM0ELtphzSkA8ck8PjCBxAYDVR0jBIG8MIG5gBQr9y+Kq4lHtM0E 17 | LtphzSkA8ck8PqGBlaSBkjCBjzELMAkGA1UEBhMCQ0ExCzAJBgNVBAgTAk5UMRQw 18 | EgYDVQQHEwtZZWxsb3drbmlmZTENMAsGA1UEChMERGVtbzENMAsGA1UECxMERGVt 19 | bzEaMBgGA1UEAxMRZGVtby5zc2ltaWNyby5jb20xIzAhBgkqhkiG9w0BCQEWFGpl 20 | cmVteWNAc3NpbWljcm8uY29tggkAvrAZ3WzgX1MwDAYDVR0TBAUwAwEB/zANBgkq 21 | hkiG9w0BAQUFAAOCAQEAhOxS1ti8/X+neasbkX0x6k+3cQ7cVmzuyALJbn+smotG 22 | kjFK0ulY/zAYhnAvLQBu625vHugW1UMIvXxpJBFOS5x/O8+B07FweJxvqclF1xcG 23 | A481xXuMcPQEvcysjY/6rJbo8PRVydCegZTWwy7PgA30gmouzLkSUkRgamcZftqR 24 | xjkQYvFvQ9YkIMLgZedpikLZ/9rp60udzAyN44FPfhGVqgIYu2wxtAnfaIYtLOgf 25 | KTQorr6PMIlOmhGu9QGGPsTen2QRukbk48isuDCV6JXyHtDmJQrsyjc61yc1sh7e 26 | ZfH1tBk4OUCauZIH9Pk+WfpFkbyjWJDSBVqsQGBuvQ== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /test/certs/rootCA.srl: -------------------------------------------------------------------------------- 1 | E9928ADA293BF8F7 2 | -------------------------------------------------------------------------------- /test/escaping.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var ldap; 8 | 9 | var dn_esc = LDAP.escapefn('dn', '#dc=%s,dc=%s'); 10 | var filter_esc =LDAP.escapefn('filter', '(objectClass=%s)'); 11 | 12 | describe('Escaping', function() { 13 | it ('Should initialize OK', function() { 14 | ldap = new LDAP({ 15 | uri: 'ldap://localhost:1234', 16 | base: 'dc=sample,dc=com', 17 | attrs: '*' 18 | }); 19 | }); 20 | it('Should escape a dn', function() { 21 | assert.equal(dn_esc('#foo', 'bar;baz'), '#dc=#foo,dc=bar\\;baz'); 22 | }); 23 | it('Should escape a filter', function() { 24 | assert.equal(filter_esc('StarCorp*'), '(objectClass=StarCorp\\2A)'); 25 | }); 26 | it('Should escape Parens', function() { 27 | var esc = LDAP.escapefn('filter', '(cn=%s)'); 28 | assert.equal(filter_esc('weird_but_legal_username_with_parens()'), 29 | '(objectClass=weird_but_legal_username_with_parens\\28\\29)'); 30 | }); 31 | it('Should escape Parens', function() { 32 | var esc = LDAP.escapefn('filter', '(cn=%s)'); 33 | assert.equal(filter_esc('weird_but_legal_username_with_parens()'), 34 | '(objectClass=weird_but_legal_username_with_parens\\28\\29)'); 35 | }); 36 | it('Should escape an obvious injection', function() { 37 | var esc = LDAP.escapefn('filter', '(cn=%s)'); 38 | assert.equal(esc('*)|(password=*)'), '(cn=\\2A\\29|\\28password=\\2A\\29)'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var fs = require('fs'); 8 | var ldap; 9 | var ldap2; 10 | 11 | // This shows an inline image for iTerm2 12 | // should not harm anything otherwise. 13 | function showImage(what) { 14 | var encoded = what.toString('base64'); 15 | 16 | process.stdout.write('\x1b]1337;File=size='+ what.length + 17 | ';width=auto;height=auto;inline=1:' + 18 | encoded + '\x07\n'); 19 | 20 | } 21 | 22 | describe('LDAP', function() { 23 | it ('Should initialize OK', function(done) { 24 | ldap = new LDAP({ 25 | uri: 'ldap://localhost:1234', 26 | base: 'dc=sample,dc=com', 27 | attrs: '*' 28 | }, done); 29 | }); 30 | it ('Should search', function(done) { 31 | ldap.search({ 32 | filter: '(cn=babs)', 33 | scope: LDAP.SUBTREE 34 | }, function(err, res) { 35 | assert.ifError(err); 36 | assert.equal(res.length, 1); 37 | assert.equal(res[0].sn[0], 'Jensen'); 38 | assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); 39 | done(); 40 | }); 41 | }); 42 | /* it ('Should timeout', function(done) { 43 | ldap.timeout=1; // 1ms should do it 44 | ldap.search('dc=sample,dc=com', '(cn=albert)', '*', function(err, msgid, res) { 45 | // assert(err !== undefined); 46 | ldap.timeout=1000; 47 | done(); 48 | }); 49 | }); */ 50 | it ('Should show TLS not active', function() { 51 | assert(ldap.tlsactive() === 0); 52 | }); 53 | it ('Should return specified attrs', function(done) { 54 | ldap.search({ 55 | base: 'dc=sample,dc=com', 56 | filter: '(cn=albert)', 57 | attrs: 'sn' 58 | }, function(err, res) { 59 | assert.ifError(err); 60 | assert.notEqual(res, undefined); 61 | assert.notEqual(res[0], undefined); 62 | assert.equal(res[0].sn[0], 'Root'); 63 | assert.equal(res[0].cn, undefined); 64 | done(); 65 | }); 66 | }); 67 | it ('Should handle a null result', function(done) { 68 | ldap.search({ 69 | base: 'dc=sample,dc=com', 70 | filter: '(cn=wontfindthis)', 71 | scope: LDAP.ONELEVEL, 72 | attrs: '*' 73 | }, function(err, res) { 74 | assert.equal(res.length, 0); 75 | done(); 76 | }); 77 | }); 78 | it ('Should not delete', function(done) { 79 | ldap.delete('cn=Albert,ou=Accounting,dc=sample,dc=com', function(err) { 80 | assert.ifError(!err); 81 | done(); 82 | }); 83 | }); 84 | it ('Should findandbind()', function(done) { 85 | ldap.findandbind({ 86 | base: 'dc=sample,dc=com', 87 | filter: '(cn=Charlie)', 88 | attrs: '*', 89 | password: 'foobarbaz' 90 | }, function(err, data) { 91 | assert.ifError(err); 92 | assert.equal(data.cn, 'Charlie'); 93 | done(); 94 | }); 95 | }); 96 | it ('Should findandbind() again', function(done) { 97 | ldap.findandbind({ 98 | base: 'dc=sample,dc=com', 99 | filter: '(cn=Charlie)', 100 | attrs: '*', 101 | password: 'foobarbaz' 102 | }, function(err, data) { 103 | assert.ifError(err); 104 | assert.equal(data.cn, 'Charlie'); 105 | done(); 106 | }); 107 | }); 108 | it ('Should fail findandbind()', function(done) { 109 | ldap.findandbind({ 110 | base: 'dc=sample,dc=com', 111 | filter: '(cn=Charlie)', 112 | attrs: 'cn', 113 | password: 'foobarbax' 114 | }, function(err, data) { 115 | assert.ifError(!err); 116 | done(); 117 | }); 118 | }); 119 | it ('Should not bind', function(done) { 120 | ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'xsecret'}, function(err) { 121 | assert.ifError(!err); 122 | done(); 123 | }); 124 | }); 125 | it ('Should bind', function(done) { 126 | ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'secret'}, function(err) { 127 | assert.ifError(err); 128 | done(); 129 | }); 130 | }); 131 | it ('Should show the rootDSE', function(done) { 132 | ldap.search({ 133 | base: '', 134 | scope: LDAP.BASE, 135 | filter: '(objectClass=*)', 136 | attrs: '+' 137 | }, function(err, data) { 138 | assert.ifError(err); 139 | assert(data[0].namingContexts[0] === 'dc=sample,dc=com'); 140 | done(); 141 | }); 142 | }); 143 | it ('Should delete', function(done) { 144 | ldap.delete('cn=Albert,ou=Accounting,dc=sample,dc=com', function(err) { 145 | assert.ifError(err); 146 | ldap.search({ 147 | base: 'dc=sample,dc=com', 148 | filter: '(cn=albert)', 149 | attrs: '*' 150 | }, function(err, res) { 151 | assert(res.length == 0); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | it ('Should add', function(done) { 157 | ldap.add('cn=Albert,ou=Accounting,dc=sample,dc=com', [ 158 | { 159 | attr: 'cn', 160 | vals: [ 'Albert' ] 161 | }, 162 | { 163 | attr: 'objectClass', 164 | vals: [ 'organizationalPerson', 'person' ] 165 | }, 166 | { 167 | attr: 'sn', 168 | vals: [ 'Root' ] 169 | }, 170 | { 171 | attr: 'userPassword', 172 | vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] 173 | } 174 | ], function(err, res) { 175 | assert.ifError(err); 176 | done(); 177 | }); 178 | 179 | }); 180 | it ('Should fail to add', function(done) { 181 | ldap.add('cn=Albert,ou=Accounting,dc=sample,dc=com', [ 182 | { 183 | attr: 'cn', 184 | vals: [ 'Albert' ] 185 | }, 186 | { 187 | attr: 'objectClass', 188 | vals: [ 'organizationalPerson', 'person' ] 189 | }, 190 | { 191 | attr: 'sn', 192 | vals: [ 'Root' ] 193 | }, 194 | { 195 | attr: 'userPassword', 196 | vals: [ 'e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9' ] 197 | } 198 | ], function(err, res) { 199 | assert.ifError(!err); 200 | done(); 201 | }); 202 | }); 203 | it ('Should survive a slight beating', function(done) { 204 | this.timeout(5000); 205 | var count = 0; 206 | for (var x = 0 ; x < 1000 ; x++) { 207 | ldap.search({ 208 | base: 'dc=sample,dc=com', 209 | filter: '(cn=albert)', 210 | attrs: '*' 211 | }, function(err, res) { 212 | count++; 213 | if (count >= 1000) { 214 | done(); 215 | } 216 | }); 217 | } 218 | }); 219 | it ('Should rename', function(done) { 220 | ldap.rename('cn=Albert,ou=Accounting,dc=sample,dc=com', 'cn=Alberto', function(err) { 221 | assert.ifError(err); 222 | ldap.rename('cn=Alberto,ou=Accounting,dc=sample,dc=com', 'cn=Albert', function(err) { 223 | assert.ifError(err); 224 | done(); 225 | }); 226 | }); 227 | }); 228 | it ('Should fail to rename', function(done) { 229 | ldap.rename('cn=Alberto,ou=Accounting,dc=sample,dc=com', 'cn=Albert', function(err) { 230 | assert.ifError(!err); 231 | done(); 232 | }); 233 | }); 234 | it ('Should modify a record', function(done) { 235 | ldap.modify('cn=Albert,ou=Accounting,dc=sample,dc=com', [ 236 | { op: 'add', attr: 'title', vals: [ 'King of Callbacks' ] }, 237 | { op: 'add', attr: 'telephoneNumber', vals: [ '18005551212', '18005551234' ] } 238 | ], function(err) { 239 | assert.ifError(err); 240 | ldap.search( 241 | { 242 | base: 'dc=sample,dc=com', 243 | filter: '(cn=albert)', 244 | attrs: '*' 245 | }, function(err, res) { 246 | assert.equal(res[0].title[0], 'King of Callbacks'); 247 | assert.equal(res[0].telephoneNumber[0], '18005551212'); 248 | assert.equal(res[0].telephoneNumber[1], '18005551234'); 249 | done(); 250 | }); 251 | }); 252 | }); 253 | it ('Should fail to modify a record', function(done) { 254 | ldap.modify('cn=Albert,ou=Accounting,dc=sample,dc=com', [ 255 | { 256 | op: 'add', 257 | attr: 'notInSchema', 258 | vals: [ 'King of Callbacks' ] 259 | } 260 | ], function(err) { 261 | assert.notEqual(err, undefined); 262 | done(); 263 | }); 264 | }); 265 | it ('Should handle a binary return', function(done) { 266 | ldap.search({ 267 | base: 'dc=sample,dc=com', 268 | filter: '(cn=babs)', 269 | attrs: 'jpegPhoto' 270 | }, function(err, res) { 271 | showImage(res[0].jpegPhoto[0]); 272 | done(); 273 | }); 274 | }); 275 | it ('Should accept unicode on modify', function(done) { 276 | ldap.modify('cn=Albert,ou=Accounting,dc=sample,dc=com', [ 277 | { op: 'replace', attr: 'title', vals: [ 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ' ] } 278 | ], function(err) { 279 | assert(!err, 'Bad unicode'); 280 | ldap.search({ 281 | base: 'dc=sample,dc=com', 282 | filter: '(cn=albert)', 283 | attrs: '*' 284 | }, function(err, res) { 285 | assert.equal(res[0].title[0], 'ᓄᓇᕗᑦ ᒐᕙᒪᖓ'); 286 | done(); 287 | }); 288 | }); 289 | }); 290 | it ('Should search with weird inputs', function(done) { 291 | ldap.search({ 292 | base: 'dc=sample,dc=com', 293 | scope: LDAP.ONELEVEL, 294 | filter: '(objectClass=*)', 295 | attrs: '+' 296 | }, function(err, res) { 297 | assert.equal(res.length, 4); 298 | done(); 299 | }); 300 | }); 301 | it ('Should close and disconnect', function() { 302 | ldap.close(); 303 | }); 304 | it ('Should connect again OK', function(done) { 305 | ldap = new LDAP({ 306 | uri: 'ldap://localhost:1234', 307 | base: 'dc=sample,dc=com', 308 | attrs: '*' 309 | }, done); 310 | }); 311 | it ('Should close again', function() { 312 | ldap.close(); 313 | }); 314 | it ('Should connect over domain socket', function(done) { 315 | ldap = new LDAP({ 316 | uri: 'ldapi://%2ftmp%2fslapd.sock', 317 | base: 'dc=sample,dc=com', 318 | attrs: '*' 319 | }, done); 320 | }); 321 | it ('Should search over domain socket', function(done) { 322 | ldap.search({ 323 | filter: '(cn=babs)', 324 | scope: LDAP.SUBTREE 325 | }, function(err, res) { 326 | assert.ifError(err); 327 | assert.equal(res.length, 1); 328 | assert.equal(res[0].sn[0], 'Jensen'); 329 | assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); 330 | done(); 331 | }); 332 | }); 333 | it ('Should survive a slight beating', function(done) { 334 | this.timeout(5000); 335 | var count = 0; 336 | for (var x = 0 ; x < 1000 ; x++) { 337 | ldap.search({ 338 | base: 'dc=sample,dc=com', 339 | filter: '(cn=albert)', 340 | attrs: '*' 341 | }, function(err, res) { 342 | count++; 343 | if (count >= 1000) { 344 | ldap.close(); 345 | done(); 346 | } 347 | }); 348 | } 349 | }); 350 | }); 351 | -------------------------------------------------------------------------------- /test/issues.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var fs = require('fs'); 8 | var ldap; 9 | 10 | var ldapConfig = { 11 | schema: 'ldaps://', 12 | host: 'localhost:1235' 13 | }; 14 | var uri = ldapConfig.schema + ldapConfig.host; 15 | 16 | 17 | describe('Issues', function() { 18 | it('Should fix Issue #80', function(done) { 19 | ldap = new LDAP({ 20 | uri: uri, 21 | validatecert: LDAP.LDAP_OPT_X_TLS_NEVER 22 | }, function (err) { 23 | assert.ifError(err); 24 | done(); 25 | }); 26 | }); 27 | it('Should search after Issue #80', function(done) { 28 | ldap.search({ 29 | base: 'dc=sample,dc=com', 30 | filter: '(objectClass=*)' 31 | }, function(err, res) { 32 | assert.ifError(err); 33 | assert.equal(res.length, 6); 34 | done(); 35 | }); 36 | 37 | }); 38 | it('Connect context should be ldap object - Issue #84', function(done) { 39 | ldap = new LDAP({ 40 | uri: uri, 41 | validatecert: LDAP.LDAP_OPT_X_TLS_NEVER, 42 | connect: function() { 43 | assert(typeof this.bind === 'function'); 44 | ldap.bind({binddn: 'cn=Manager,dc=sample,dc=com', password: 'secret'}, function(err) { 45 | assert.ifError(err); 46 | done(); 47 | }); 48 | } 49 | }, function (err) { 50 | assert.ifError(err); 51 | }); 52 | }); 53 | it('Base scope should work - Issue #81', function(done) { 54 | assert.equal(ldap.DEFAULT, 4, 'ldap.DEFAULT const is not zero'); 55 | assert.equal(LDAP.DEFAULT, 4, 'LDAP.DEFAULT const is not zero'); 56 | assert.equal(LDAP.LDAP_OPT_X_TLS_TRY, 4); 57 | ldap.search({ 58 | base: 'dc=sample,dc=com', 59 | scope: ldap.BASE, 60 | filter: '(objectClass=*)' 61 | }, function(err, res) { 62 | 63 | assert.equal(res.length, 1, 'Unexpected number of results'); 64 | ldap.search({ 65 | base: 'dc=sample,dc=com', 66 | scope: LDAP.SUBTREE, 67 | filter: '(objectClass=*)' 68 | }, function(err, res) { 69 | assert.ifError(err); 70 | assert.equal(res.length, 6, 'Unexpected number of results'); 71 | ldap.search({ 72 | base: 'dc=sample,dc=com', 73 | scope: LDAP.ONELEVEL, 74 | filter: '(objectClass=*)' 75 | }, function(err, res) { 76 | assert.ifError(err); 77 | assert.equal(res.length, 4, 'Unexpected number of results'); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/ldaps.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var fs = require('fs'); 8 | var ldap; 9 | 10 | describe('LDAP TLS', function() { 11 | it ('Should fail TLS on cert validation', function(done) { 12 | this.timeout(10000); 13 | ldap = new LDAP({ 14 | uri: 'ldaps://localhost:1235', 15 | base: 'dc=sample,dc=com', 16 | attrs: '*', 17 | }, function(err) { 18 | assert.ifError(!err); 19 | done(); 20 | }); 21 | }); 22 | it ('Should connect', function(done) { 23 | this.timeout(10000); 24 | ldap = new LDAP({ 25 | uri: 'ldaps://localhost:1235', 26 | base: 'dc=sample,dc=com', 27 | attrs: '*', 28 | validatecert: false 29 | }, function(err) { 30 | assert.ifError(err); 31 | done(); 32 | }); 33 | }); 34 | it ('Should search via TLS', function(done) { 35 | ldap.search({ 36 | filter: '(cn=babs)', 37 | scope: LDAP.SUBTREE 38 | }, function(err, res) { 39 | assert.ifError(err); 40 | assert.equal(res.length, 1); 41 | assert.equal(res[0].sn[0], 'Jensen'); 42 | assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); 43 | done(); 44 | }); 45 | }); 46 | it ('Should findandbind()', function(done) { 47 | ldap.findandbind({ 48 | base: 'dc=sample,dc=com', 49 | filter: '(cn=Charlie)', 50 | attrs: '*', 51 | password: 'foobarbaz' 52 | }, function(err, data) { 53 | assert.ifError(err); 54 | done(); 55 | }); 56 | }); 57 | it ('Should fail findandbind()', function(done) { 58 | ldap.findandbind({ 59 | base: 'dc=sample,dc=com', 60 | filter: '(cn=Charlie)', 61 | attrs: 'cn', 62 | password: 'foobarbax' 63 | }, function(err, data) { 64 | assert.ifError(!err); 65 | done(); 66 | }); 67 | }); 68 | it ('Should still have TLS', function() { 69 | assert(ldap.tlsactive()); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/leakcheck: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var ldap; 8 | var errors = {};; 9 | 10 | ldap = new LDAP({ 11 | uri: 'ldaps://localhost:1235', 12 | starttls: false, 13 | verifycert: false 14 | }); 15 | setInterval(function() { 16 | ldap.search({ 17 | base: 'dc=sample,dc=com', 18 | filter: '(objectClass=*)', 19 | scope: LDAP.SUBTREE 20 | }, function(err, res) { 21 | if (err) { 22 | if (!errors[err.message]) { 23 | errors[err.message] = 0; 24 | } 25 | errors[err.message]++; 26 | // assert(ldap.tlsactive()); 27 | return; 28 | } 29 | }); 30 | }, 10); 31 | 32 | setInterval(function() { 33 | console.log('✓ ' + new Date()); 34 | console.log(ldap.stats); 35 | console.log(errors); 36 | console.log(''); 37 | }, 10000); 38 | 39 | -------------------------------------------------------------------------------- /test/run_sasl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z $SLAPD ]] ; then 4 | SLAPD=/usr/local/libexec/slapd 5 | fi 6 | 7 | if [[ -z $SLAPADD ]] ; then 8 | SLAPADD=/usr/local/sbin/slapadd 9 | fi 10 | 11 | if [[ -z $SLAPD_CONF ]] ; then 12 | SLAPD_CONF=sasl.conf 13 | fi 14 | 15 | MKDIR=/bin/mkdir 16 | RM=/bin/rm 17 | 18 | $RM -rf openldap-data 19 | $MKDIR openldap-data 20 | 21 | if [[ -f slapd.pid ]] ; then 22 | $RM slapd.pid 23 | fi 24 | 25 | $SLAPADD -f $SLAPD_CONF < startup.ldif 26 | $SLAPADD -f $SLAPD_CONF < sasl.ldif 27 | $SLAPD -d999 -f $SLAPD_CONF -hldap://localhost:1234 > sasl.log 2>&1 & 28 | 29 | if [[ ! -f slapd.pid ]] ; then 30 | sleep 1 31 | fi 32 | 33 | # Make sure SASL is enabled 34 | if ldapsearch -H ldap://localhost:1234 -x -b "" -s base -LLL \ 35 | supportedSASLMechanisms | grep -q SASL ; then 36 | : 37 | else 38 | echo slapd started but SASL not supported 39 | fi 40 | -------------------------------------------------------------------------------- /test/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SLAPD=/usr/local/libexec/slapd 4 | SLAPADD=/usr/local/sbin/slapadd 5 | MKDIR=/bin/mkdir 6 | RM=/bin/rm 7 | KILL=/bin/kill 8 | 9 | $RM -rf openldap-data 10 | $MKDIR openldap-data 11 | 12 | $SLAPADD -f slapd.conf < startup.ldif 13 | $SLAPD -d999 -f slapd.conf -h "ldap://:1234 ldapi://%2ftmp%2fslapd.sock ldaps://localhost:1235" 14 | SLAPD_PID=$! 15 | # slapd should be running now 16 | 17 | -------------------------------------------------------------------------------- /test/sasl.conf: -------------------------------------------------------------------------------- 1 | include /usr/local/etc/openldap/schema/core.schema 2 | include /usr/local/etc/openldap/schema/cosine.schema 3 | include /usr/local/etc/openldap/schema/inetorgperson.schema 4 | 5 | pidfile ./slapd.pid 6 | argsfile ./slapd.args 7 | 8 | modulepath /usr/local/libexec/openldap 9 | moduleload back_bdb 10 | 11 | idletimeout 100 12 | 13 | database bdb 14 | 15 | sasl-auxprops slapd 16 | sasl-secprops none 17 | authz-regexp uid=(.*),cn=PLAIN,cn=auth cn=$1,dc=sample,dc=com 18 | authz-regexp uid=(.*),cn=authz,cn=auth cn=$1,dc=sample,dc=com 19 | password-hash {CLEARTEXT} 20 | authz-policy from 21 | 22 | suffix "dc=sample,dc=com" 23 | rootdn "cn=Manager,dc=sample,dc=com" 24 | rootpw secret 25 | directory ./openldap-data 26 | index objectClass,cn,contextCSN eq 27 | -------------------------------------------------------------------------------- /test/sasl.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | 7 | var assert = require('assert'); 8 | 9 | var ldap; 10 | 11 | // Does not need to support GSSAPI 12 | var uri = process.env.TEST_SASL_URI || 'ldap://localhost:1234'; 13 | 14 | describe('SASL PLAIN bind', function() { 15 | connect(uri); 16 | it('Should bind with user', function(done) { 17 | ldap.saslbind({ 18 | mechanism: 'PLAIN', 19 | user: 'test_user', 20 | password: 'secret', 21 | securityproperties: 'none' 22 | }, function(err) { 23 | assert.ifError(err); 24 | done(); 25 | }); 26 | }); 27 | search(); 28 | after(cleanup); 29 | }); 30 | 31 | describe('LDAP SASL Proxy User', function() { 32 | connect(uri); 33 | it('Should bind with proxy user', function(done) { 34 | ldap.saslbind({ 35 | mechanism: 'PLAIN', 36 | user: 'test_user', 37 | password: 'secret', 38 | proxyuser: 'u:test_admin', 39 | securityproperties: 'none' 40 | }, function(err) { 41 | assert.ifError(err); 42 | done(); 43 | }); 44 | }); 45 | search(); 46 | after(cleanup); 47 | }); 48 | 49 | describe('SASL Error Handling', function() { 50 | 51 | connect(uri); 52 | 53 | it('Should fail to bind invalid password', function(done) { 54 | ldap.saslbind({ 55 | mechanism: 'PLAIN', 56 | user: 'test_user', 57 | password: 'bad password', 58 | securityproperties: 'none' 59 | }, function(err) { 60 | assert.ifError(!err); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('Should fail to bind invalid proxy user', function(done) { 66 | ldap.saslbind({ 67 | mechanism: 'PLAIN', 68 | user: 'test_user', 69 | password: 'secret', 70 | proxyuser: 'no_user', 71 | securityproperties: 'none' 72 | }, function(err) { 73 | assert.ifError(!err); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('Should throw on invalid mechanism', function(done) { 79 | try { 80 | ldap.saslbind({ mechanism: 'INVALID' }, function(err) { 81 | assert(false); 82 | }); 83 | } 84 | catch(err) { 85 | } 86 | done(); 87 | }) 88 | 89 | it('Should throw on invalid parameter', function(done) { 90 | try { 91 | ldap.saslbind({realm: 0}, function(err) { 92 | assert(false); 93 | }); 94 | } 95 | catch(err) { 96 | } 97 | done(); 98 | }); 99 | 100 | after(cleanup); 101 | }); 102 | 103 | // Needs to be a server that supports SASL authentication with default 104 | // credentials (e.g. GSSAPI) 105 | var gssapi_uri = process.env.TEST_SASL_GSSAPI_URI; 106 | if(gssapi_uri) { 107 | describe('LDAP SASL GSSAPI', function() { 108 | connect(gssapi_uri); 109 | it('Should bind with default credentials', function(done) { 110 | this.timeout(10000); 111 | ldap.saslbind(function(err) { 112 | assert.ifError(err); 113 | done(); 114 | }); 115 | }); 116 | search(); 117 | after(cleanup); 118 | }); 119 | } 120 | 121 | function connect(uri) { 122 | it('Should connect', function(done) { 123 | ldap = new LDAP({ uri: uri }, function(err) { 124 | assert.ifError(err); 125 | done(); 126 | }); 127 | }); 128 | } 129 | 130 | function search() { 131 | var dc; 132 | it('Should be able to get root info', function(done) { 133 | ldap.search({ 134 | base: '', 135 | scope: LDAP.BASE, 136 | attrs: 'namingContexts' 137 | }, function(err, res) { 138 | assert.ifError(err); 139 | assert(res.length); 140 | var ctx = res[0].namingContexts.filter(function(c) { 141 | return c.indexOf('{') < 0; // Avoid AD config context 142 | }); 143 | dc = ctx[0]; 144 | done(); 145 | }); 146 | }); 147 | it('Should be able to search', function(done) { 148 | ldap.search({ 149 | filter: '(objectClass=*)', 150 | base: dc, 151 | scope: LDAP.ONELEVEL, 152 | attrs: 'cn' 153 | }, function(err, res) { 154 | assert.ifError(err); 155 | assert(res.length); 156 | done(); 157 | }); 158 | }); 159 | } 160 | 161 | function cleanup() { 162 | if(ldap) { 163 | ldap.close(); 164 | ldap = undefined; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /test/sasl.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=test_user,dc=sample,dc=com 2 | objectClass: organizationalPerson 3 | objectClass: person 4 | objectClass: top 5 | cn: test_user 6 | sn: test_user 7 | userPassword: secret 8 | 9 | dn: cn=test_admin,dc=sample,dc=com 10 | objectClass: organizationalPerson 11 | objectClass: person 12 | objectClass: top 13 | cn: test_admin 14 | sn: test_admin 15 | authzFrom: u:test_user 16 | -------------------------------------------------------------------------------- /test/slapd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # See slapd.conf(5) for details on configuration options. 3 | # This file should NOT be world readable. 4 | # 5 | include /usr/local/etc/openldap/schema/core.schema 6 | include /usr/local/etc/openldap/schema/cosine.schema 7 | include /usr/local/etc/openldap/schema/inetorgperson.schema 8 | # Define global ACLs to disable default read access. 9 | 10 | TLSCACertificateFile certs/rootCA.pem 11 | TLSCertificateFile certs/device.crt 12 | TLSCertificateKeyFile certs/device.key 13 | 14 | # Do not enable referrals until AFTER you have a working directory 15 | # service AND an understanding of referrals. 16 | #referral ldap://root.openldap.org 17 | 18 | pidfile ./slapd.pid 19 | argsfile ./slapd.args 20 | 21 | # Load dynamic backend modules: 22 | modulepath /usr/local/libexec/openldap 23 | moduleload back_mdb 24 | # moduleload back_hdb 25 | # moduleload back_ldap 26 | 27 | timelimit 10 28 | 29 | TLSCACertificateFile certs/rootCA.pem 30 | TLSCertificateFile certs/device.crt 31 | TLSCertificateKeyFile certs/device.key 32 | 33 | 34 | # Sample security restrictions 35 | # Require integrity protection (prevent hijacking) 36 | # Require 112-bit (3DES or better) encryption for updates 37 | # Require 63-bit encryption for simple bind 38 | # security ssf=1 update_ssf=112 simple_bind=64 39 | 40 | # Sample access control policy: 41 | # Root DSE: allow anyone to read it 42 | # Subschema (sub)entry DSE: allow anyone to read it 43 | # Other DSEs: 44 | # Allow self write access 45 | # Allow authenticated users read access 46 | # Allow anonymous users to authenticate 47 | # Directives needed to implement policy: 48 | access to dn.base="" by * read 49 | access to dn.base="cn=Subschema" by * read 50 | access to * 51 | by self write 52 | by users read 53 | by anonymous read 54 | # 55 | # if no access controls are present, the default policy 56 | # allows anyone and everyone to read anything but restricts 57 | # updates to rootdn. (e.g., "access to * by * read") 58 | # 59 | # rootdn can always read and write EVERYTHING! 60 | 61 | idletimeout 100 62 | 63 | ####################################################################### 64 | # BDB database definitions 65 | ####################################################################### 66 | 67 | database mdb 68 | # overlay syncprov 69 | # syncprov-checkpoint 10 10 70 | 71 | suffix "dc=sample,dc=com" 72 | rootdn "cn=Manager,dc=sample,dc=com" 73 | # Cleartext passwords, especially for the rootdn, should 74 | # be avoid. See slappasswd(8) and slapd.conf(5) for details. 75 | # Use of strong authentication encouraged. 76 | rootpw secret 77 | # The database directory MUST exist prior to running slapd AND 78 | # should only be accessible by the slapd and slap tools. 79 | # Mode 700 recommended. 80 | directory ./openldap-data 81 | # Indices to maintain 82 | index objectClass,cn,contextCSN eq 83 | -------------------------------------------------------------------------------- /test/startup.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=sample,dc=com 2 | objectClass: dcObject 3 | objectClass: organization 4 | dc: sample 5 | o: Sample Company 6 | 7 | dn: cn=Charlie,dc=sample,dc=com 8 | objectClass: organizationalPerson 9 | objectClass: person 10 | objectClass: top 11 | cn: Charlie 12 | sn: Root 13 | userPassword: {sha}X1UT+IIv2+UUWvM7ZNjZcNz5XG4= 14 | 15 | dn: cn=Babs,dc=sample,dc=com 16 | objectClass: organizationalPerson 17 | objectClass: inetorgperson 18 | objectClass: top 19 | cn: Babs 20 | sn: Jensen 21 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 22 | jpegPhoto:: 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 259 | DwSv0VfvfyX91/yvN8X/P+P+eH/Llf/9k= 260 | 261 | dn: ou=Accounting,dc=sample,dc=com 262 | objectClass: organizationalUnit 263 | objectClass: top 264 | ou: Accounting 265 | 266 | dn: cn=Albert,ou=Accounting,dc=sample,dc=com 267 | objectClass: organizationalPerson 268 | objectClass: person 269 | objectClass: top 270 | cn: Albert 271 | sn: Root 272 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 273 | 274 | dn: cn=Manager,dc=sample,dc=com 275 | objectClass: organizationalPerson 276 | objectClass: person 277 | objectClass: top 278 | cn: Manager 279 | sn: Root 280 | userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9 281 | 282 | #dn: dc=666,dc=sample,dc=com 283 | #objectClass: dcObject 284 | #objectClass: referral 285 | #objectClass: top 286 | #dc: 666 287 | #ref: ldap://yk-ldap0.ssimicro.com/dc=666,dc=ssi 288 | -------------------------------------------------------------------------------- /test/tls.js: -------------------------------------------------------------------------------- 1 | /*jshint globalstrict:true, node:true, trailing:true, mocha:true unused:true */ 2 | 3 | 'use strict'; 4 | 5 | var LDAP = require('../'); 6 | var assert = require('assert'); 7 | var fs = require('fs'); 8 | var ldap; 9 | 10 | describe('LDAP TLS', function() { 11 | /* 12 | this succeeds, but it shouldn't 13 | starttls is beta - at best - right now... 14 | it ('Should fail TLS on cert validation', function(done) { 15 | this.timeout(10000); 16 | ldap = new LDAP({ 17 | uri: 'ldap://localhost:1234', 18 | base: 'dc=sample,dc=com', 19 | attrs: '*' 20 | }, function(err) { 21 | ldap.starttls(function(err) { 22 | console.log('ERR', err); 23 | assert.ifError(err); 24 | ldap.installtls(); 25 | assert(ldap.tlsactive() == 1); 26 | done(); 27 | }); 28 | }); 29 | }); */ 30 | it ('Should connect', function(done) { 31 | this.timeout(10000); 32 | ldap = new LDAP({ 33 | uri: 'ldap://localhost:1234', 34 | base: 'dc=sample,dc=com', 35 | attrs: '*', 36 | validatecert: false 37 | }, function(err) { 38 | assert.ifError(err); 39 | ldap.starttls(function(err) { 40 | assert.ifError(err); 41 | ldap.installtls(); 42 | assert(ldap.tlsactive()); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | it ('Should search via TLS', function(done) { 48 | ldap.search({ 49 | filter: '(cn=babs)', 50 | scope: LDAP.SUBTREE 51 | }, function(err, res) { 52 | assert.ifError(err); 53 | assert.equal(res.length, 1); 54 | assert.equal(res[0].sn[0], 'Jensen'); 55 | assert.equal(res[0].dn, 'cn=Babs,dc=sample,dc=com'); 56 | done(); 57 | }); 58 | }); 59 | it ('Should findandbind()', function(done) { 60 | ldap.findandbind({ 61 | base: 'dc=sample,dc=com', 62 | filter: '(cn=Charlie)', 63 | attrs: '*', 64 | password: 'foobarbaz' 65 | }, function(err, data) { 66 | assert.ifError(err); 67 | done(); 68 | }); 69 | }); 70 | it ('Should fail findandbind()', function(done) { 71 | ldap.findandbind({ 72 | base: 'dc=sample,dc=com', 73 | filter: '(cn=Charlie)', 74 | attrs: 'cn', 75 | password: 'foobarbax' 76 | }, function(err, data) { 77 | assert.ifError(!err); 78 | done(); 79 | }); 80 | }); 81 | it ('Should still have TLS', function() { 82 | assert(ldap.tlsactive()); 83 | ldap.close(); 84 | }); 85 | }); 86 | --------------------------------------------------------------------------------