]> jfr.im git - irc/evilnet/x3.git/blob - src/x3ldap.c
remove errors on remove non-existant, or add already exist in ldap
[irc/evilnet/x3.git] / src / x3ldap.c
1 /* x3ldap.c - LDAP functionality for x3, by Rubin
2 * Copyright 2002-2007 x3 Development Team
3 *
4 * This file is part of x3.
5 *
6 * x3 is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with srvx; if not, write to the Free Software Foundation,
18 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
19 *
20 *
21 * TODO:
22 * * get queries working in static existance, so i understand how it works
23 * * get ldap enabled in ./configure
24 * * x3.conf settings to enable/configure its use
25 * * generic functions to enable ldap
26 * * nickserv.c work to use said functions.
27 */
28
29 #include "config.h"
30 #ifdef WITH_LDAP
31
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <ldap.h>
35
36 #include "conf.h"
37 #include "global.h"
38 #include "log.h"
39 #include "x3ldap.h"
40
41 extern struct nickserv_config nickserv_conf;
42
43
44 LDAP *ld = NULL;
45
46 int ldap_do_init()
47 {
48 if(!nickserv_conf.ldap_enable)
49 return false;
50 /* TODO: check here for all required config options and exit() out if not present */
51 //ld = ldap_init(nickserv_conf.ldap_host, nickserv_conf.ldap_port);
52
53 //if(ld == NULL) {
54 if(ldap_initialize(&ld, nickserv_conf.ldap_uri)) {
55 log_module(MAIN_LOG, LOG_ERROR, "LDAP initilization failed!\n");
56 exit(1);
57 }
58 ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &nickserv_conf.ldap_version);
59 log_module(MAIN_LOG, LOG_INFO, "Success! ldap_init() was successfull in connecting to %s\n", nickserv_conf.ldap_uri);
60 return true;
61 }
62
63
64 /* Try to auth someone. If theres problems, try reconnecting
65 * once every 10 seconds for 1 minute.
66 * TODO: move this stuff to config file
67 */
68 unsigned int ldap_do_bind( const char *dn, const char *pass)
69 {
70 int q;
71
72 int n = 0;
73 while(1) {
74 q = ldap_simple_bind_s(ld, dn, pass);
75 if(q == LDAP_SUCCESS) {
76 log_module(MAIN_LOG, LOG_DEBUG, "bind() successfull! You are bound as %s", dn);
77 /* unbind now */
78 return q;
79 }
80 else if(q == LDAP_INVALID_CREDENTIALS) {
81 return q;
82 }
83 else {
84 log_module(MAIN_LOG, LOG_ERROR, "Bind failed: %s/****** (%s)", dn, ldap_err2string(q));
85 /* ldap_perror(ld, "ldap"); */
86 ldap_do_init();
87 }
88 if(n++ > 1) {
89 /* TODO: return to the user that this is a connection error and not a problem
90 * with their password
91 */
92 log_module(MAIN_LOG, LOG_ERROR, "Failing to reconnect to ldap server. Auth failing.");
93 return q;
94 }
95 }
96 log_module(MAIN_LOG, LOG_ERROR, "ldap_do_bind falling off the end. this shouldnt happen");
97 return q;
98 }
99 int ldap_do_admin_bind()
100 {
101 if(!(nickserv_conf.ldap_admin_dn && *nickserv_conf.ldap_admin_dn &&
102 nickserv_conf.ldap_admin_pass && *nickserv_conf.ldap_admin_pass)) {
103 log_module(MAIN_LOG, LOG_ERROR, "Tried to admin bind, but no admin credentials configured in config file. ldap_admin_dn/ldap_admin_pass");
104 return LDAP_OTHER; /* not configured to do this */
105 }
106 return(ldap_do_bind(nickserv_conf.ldap_admin_dn, nickserv_conf.ldap_admin_pass));
107 }
108
109
110 unsigned int ldap_check_auth( char *account, char *pass)
111 {
112 char buff[MAXLEN];
113
114 if(!nickserv_conf.ldap_enable)
115 return LDAP_OTHER;
116
117 memset(buff, 0, MAXLEN);
118 snprintf(buff, sizeof(buff)-1, nickserv_conf.ldap_dn_fmt /*"uid=%s,ou=Users,dc=afternet,dc=org"*/, account);
119 return ldap_do_bind(buff, pass);
120
121 }
122
123 int ldap_search_user(char *account, LDAPMessage **entry)
124 {
125
126 char filter[MAXLEN+1];
127 int rc;
128 LDAPMessage *res;
129
130 struct timeval timeout;
131
132 memset(filter, 0, MAXLEN+1);
133 snprintf(filter, MAXLEN, "%s=%s", nickserv_conf.ldap_field_account, account);
134 /*
135 Now we do a search;
136 */
137 timeout.tv_usec = 0;
138 timeout.tv_sec = nickserv_conf.ldap_timeout;
139 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
140 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
141 return rc;
142 }
143 if( (rc = ldap_search_st(ld, nickserv_conf.ldap_base, LDAP_SCOPE_ONELEVEL, filter, NULL, 0, &timeout, &res)) != LDAP_SUCCESS) {
144 log_module(MAIN_LOG, LOG_ERROR, "search failed: %s %s: %s", nickserv_conf.ldap_base, filter, ldap_err2string(rc));
145 return(rc);
146 }
147 log_module(MAIN_LOG, LOG_DEBUG, "Search successfull! %s %s\n", nickserv_conf.ldap_base, filter);
148 if(ldap_count_entries(ld, res) != 1) {
149 log_module(MAIN_LOG, LOG_ERROR, "LDAP search got %d entries when looking for %s", ldap_count_entries(ld, res), account);
150 return(LDAP_OTHER); /* Search was a success, but user not found.. */
151 }
152 log_module(MAIN_LOG, LOG_DEBUG, "LDAP search got %d entries", ldap_count_entries(ld, res));
153 *entry = ldap_first_entry(ld, res);
154 return(rc);
155 }
156
157 /* queries the ldap server for account..
158 * if a single account match is found,
159 * email is allocated and set to the email address
160 * and returns LDAP_SUCCESS. returns LDAP_OTHER if
161 * 0 or 2+ entries are matched, or the proper ldap error
162 * code for other errors.
163 */
164 int ldap_get_user_info(char *account, char **email)
165 {
166 int rc;
167 char **value;
168 LDAPMessage *entry, *res;
169 *email = NULL;
170 if( (rc = ldap_search_user(account, &res)) == LDAP_SUCCESS) {
171 entry = ldap_first_entry(ld, res);
172 value = ldap_get_values(ld, entry, nickserv_conf.ldap_field_email);
173 if(!value) {
174 return(LDAP_OTHER);
175 }
176 *email = strdup(value[0]);
177 log_module(MAIN_LOG, LOG_DEBUG, "%s: %s\n", nickserv_conf.ldap_field_email, value[0]);
178 /*
179 value = ldap_get_values(ld, entry, "description");
180 log_module(MAIN_LOG, LOG_DEBUG, "Description: %s\n", value[0]);
181 value = ldap_get_values(ld, entry, "userPassword");
182 log_module(MAIN_LOG, LOG_DEBUG, "pass: %s\n", value ? value[0] : "error");
183 */
184 }
185 return(rc);
186 }
187
188 /*
189 ldap_result();
190 ldap_first_entry();
191 ldap_first_attribute();
192 for(;;) {
193 ldap_get_values();
194 ldap_next_attribute();
195 }
196
197 ldap_parse_result();
198
199 ldap_unbind_ext();
200 */
201 /* get errors with ldap_err2string(); */
202
203
204 /********* base64 stuff ***********/
205
206 unsigned char *pack(const char *str, unsigned int *len)
207 {
208 int nibbleshift = 4;
209 int first = 1;
210 char *v;
211 static unsigned char buf[MAXLEN+1];
212 int outputpos = -1;
213
214 memset(buf, 0, MAXLEN+1);
215 v = (char *)str;
216 while(*v) {
217 char n = *(v++);
218
219 if((n >= '0') && (n <= '9')) {
220 n -= '0';
221 } else if ((n >= 'A') && (n <= 'F')) {
222 n -= ('A' - 10);
223 } else if ((n >= 'a') && (n <= 'f')) {
224 n -= ('a' - 10);
225 } else {
226 printf("pack type H: illegal hex digit %c", n);
227 n = 0;
228 }
229
230 if (first--) {
231 buf[++outputpos] = 0;
232 } else {
233 first = 1;
234 }
235
236 buf[outputpos] |= (n << nibbleshift);
237 nibbleshift = (nibbleshift + 4) & 7;
238 }
239 *len = outputpos+1;
240 return(buf);
241 }
242
243
244 /* from php5 sources */
245 static char base64_table[] =
246 { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
247 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
248 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
249 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
250 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '\0'
251 };
252 static char base64_pad = '=';
253
254 char *base64_encode(const unsigned char *str, int length, int *ret_length)
255 {
256 const unsigned char *current = str;
257 char *p;
258 char *result;
259
260 if ((length + 2) < 0 || ((length + 2) / 3) >= (1 << (sizeof(int) * 8 - 2))) {
261 if (ret_length != NULL) {
262 *ret_length = 0;
263 }
264 return NULL;
265 }
266
267 result = (char *)calloc(((length + 2) / 3) * 4, sizeof(char));
268 p = result;
269
270 while (length > 2) { /* keep going until we have less than 24 bits */
271 *p++ = base64_table[current[0] >> 2];
272 *p++ = base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
273 *p++ = base64_table[((current[1] & 0x0f) << 2) + (current[2] >> 6)];
274 *p++ = base64_table[current[2] & 0x3f];
275
276 current += 3;
277 length -= 3; /* we just handle 3 octets of data */
278 }
279
280 /* now deal with the tail end of things */
281 if (length != 0) {
282 *p++ = base64_table[current[0] >> 2];
283 if (length > 1) {
284 *p++ = base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
285 *p++ = base64_table[(current[1] & 0x0f) << 2];
286 *p++ = base64_pad;
287 } else {
288 *p++ = base64_table[(current[0] & 0x03) << 4];
289 *p++ = base64_pad;
290 *p++ = base64_pad;
291 }
292 }
293 if (ret_length != NULL) {
294 *ret_length = (int)(p - result);
295 }
296 *p = '\0';
297 return result;
298 }
299
300
301 char **make_object_vals()
302 {
303 unsigned int y;
304 static char **object_vals = NULL;
305
306 if(object_vals)
307 free(object_vals);
308
309 object_vals = malloc(sizeof( *object_vals ) * nickserv_conf.ldap_object_classes->used);
310
311 for(y = 0; y < nickserv_conf.ldap_object_classes->used; y++) {
312 object_vals[y] = nickserv_conf.ldap_object_classes->list[y];
313 }
314 object_vals[y] = NULL;
315 return object_vals;
316 }
317
318 char *make_password(const char *crypted)
319 {
320 char *base64pass;
321 unsigned char *packed;
322 unsigned int len;
323 char *passbuf;
324
325 packed = pack(crypted, &len);
326 base64pass = base64_encode(packed, len, NULL);
327 passbuf = malloc(strlen(base64pass) + 1 + 5);
328 strcpy(passbuf, "{MD5}");
329 strcat(passbuf, base64pass);
330 //log_module(MAIN_LOG, LOG_DEBUG, "Encoded password is: '%s'", passbuf);
331 free(base64pass);
332 return passbuf;
333
334 }
335
336 LDAPMod **make_mods_add(const char *account, const char *password, const char *email, int *num_mods_ret)
337 {
338 static char *account_vals[] = { NULL, NULL };
339 static char *password_vals[] = { NULL, NULL };
340 static char *email_vals[] = { NULL, NULL };
341 int num_mods = 3;
342 int i;
343 /* TODO: take this from nickserv_conf.ldap_add_objects */
344 LDAPMod **mods;
345 static char **object_vals;
346 object_vals = make_object_vals();
347
348 account_vals[0] = (char *) account;
349 password_vals[0] = (char *) password;
350 email_vals[0] = (char *) email;
351
352 if(!(nickserv_conf.ldap_field_account && *nickserv_conf.ldap_field_account))
353 return 0; /* account required */
354 if(!(nickserv_conf.ldap_field_password && *nickserv_conf.ldap_field_password))
355 return 0; /* password required */
356 if(email && *email && nickserv_conf.ldap_field_email && *nickserv_conf.ldap_field_email)
357 num_mods++;
358
359 mods = ( LDAPMod ** ) malloc(( num_mods + 1 ) * sizeof( LDAPMod * ));
360 for( i = 0; i < num_mods; i++) {
361 mods[i] = (LDAPMod *) malloc(sizeof(LDAPMod));
362 memset(mods[i], 0, sizeof(LDAPMod));
363 }
364
365 mods[0]->mod_op = LDAP_MOD_ADD;
366 mods[0]->mod_type = strdup("objectclass");
367 mods[0]->mod_values = object_vals;
368
369 mods[1]->mod_op = LDAP_MOD_ADD;
370 mods[1]->mod_type = strdup(nickserv_conf.ldap_field_account);
371 mods[1]->mod_values = account_vals;
372
373 mods[2]->mod_op = LDAP_MOD_ADD;
374 mods[2]->mod_type = strdup(nickserv_conf.ldap_field_password);
375 mods[2]->mod_values = password_vals;
376
377 if(nickserv_conf.ldap_field_email && *nickserv_conf.ldap_field_email && email && *email) {
378 mods[3]->mod_op = LDAP_MOD_ADD;
379 mods[3]->mod_type = strdup(nickserv_conf.ldap_field_email);
380 mods[3]->mod_values = email_vals;
381 mods[4] = NULL;
382 }
383 else
384 mods[3] = NULL;
385 *num_mods_ret = num_mods;
386 return mods;
387 }
388
389 int ldap_do_add(const char *account, const char *crypted, const char *email)
390 {
391 char newdn[MAXLEN];
392 LDAPMod **mods;
393 int rc, i;
394 int num_mods;
395 char *passbuf;
396
397 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
398 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
399 return rc;
400 }
401
402 passbuf = make_password(crypted);
403 snprintf(newdn, MAXLEN-1, nickserv_conf.ldap_dn_fmt, account);
404 mods = make_mods_add(account, passbuf, email, &num_mods);
405 if(!mods) {
406 log_module(MAIN_LOG, LOG_ERROR, "Error building mods for ldap_add");
407 return LDAP_OTHER;
408 }
409 rc = ldap_add_ext_s(ld, newdn, mods, NULL, NULL);
410 if(rc != LDAP_SUCCESS && rc!= LDAP_ALREADY_EXISTS) {
411 log_module(MAIN_LOG, LOG_ERROR, "Error adding ldap account: %s -- %s", account, ldap_err2string(rc));
412 return rc;
413 }
414 //ldap_unbind_s(ld);
415 for(i = 0; i < num_mods; i++) {
416 free(mods[i]->mod_type);
417 free(mods[i]);
418 }
419 free(mods);
420 free(passbuf);
421 return rc;
422 }
423
424 int ldap_delete_account(char *account)
425 {
426 char dn[MAXLEN];
427 int rc;
428
429 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
430 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
431 return rc;
432 }
433
434 memset(dn, 0, MAXLEN);
435 snprintf(dn, MAXLEN-1, nickserv_conf.ldap_dn_fmt, account);
436 return(ldap_delete_s(ld, dn));
437 }
438
439 int ldap_rename_account(char *oldaccount, char *newaccount)
440 {
441 char dn[MAXLEN], newdn[MAXLEN];
442 int rc;
443
444 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
445 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
446 return rc;
447 }
448
449 memset(dn, 0, MAXLEN);
450 memset(newdn, 0, MAXLEN);
451 snprintf(dn, MAXLEN-1, nickserv_conf.ldap_dn_fmt, oldaccount);
452 strcat(newdn, nickserv_conf.ldap_field_account);
453 strcat(newdn, "=");
454 strcat(newdn, newaccount);
455 rc = ldap_modrdn2_s(ld, dn, newdn, true);
456 if(rc != LDAP_SUCCESS) {
457 log_module(MAIN_LOG, LOG_ERROR, "Error modifying ldap account: %s -- %s", oldaccount, ldap_err2string(rc));
458 return rc;
459 }
460 return rc;
461
462 }
463
464 LDAPMod **make_mods_modify(const char *password, const char *email, int *num_mods_ret)
465 {
466 static char *password_vals[] = { NULL, NULL };
467 static char *email_vals[] = { NULL, NULL };
468 int num_mods = 0;
469 int i;
470 /* TODO: take this from nickserv_conf.ldap_add_objects */
471 LDAPMod **mods;
472
473 password_vals[0] = (char *) password;
474 email_vals[0] = (char *) email;
475
476 if(!(nickserv_conf.ldap_field_password && *nickserv_conf.ldap_field_password))
477 return 0; /* password required */
478 /*
479 if(email && *email && nickserv_conf.ldap_field_email && *nickserv_conf.ldap_field_email)
480 num_mods++;
481 */
482 if(password)
483 num_mods++;
484 if(email)
485 num_mods++;
486
487 mods = ( LDAPMod ** ) malloc(( num_mods + 1 ) * sizeof( LDAPMod * ));
488 for( i = 0; i < num_mods; i++) {
489 mods[i] = (LDAPMod *) malloc(sizeof(LDAPMod));
490 memset(mods[i], 0, sizeof(LDAPMod));
491 }
492
493 i = 0;
494 if(nickserv_conf.ldap_field_password && *nickserv_conf.ldap_field_password &&
495 password) {
496 mods[i]->mod_op = LDAP_MOD_REPLACE;
497 mods[i]->mod_type = strdup(nickserv_conf.ldap_field_password);
498 mods[i]->mod_values = password_vals;
499 i++;
500 }
501
502 if(nickserv_conf.ldap_field_email && *nickserv_conf.ldap_field_email && email) {
503 mods[i]->mod_op = LDAP_MOD_REPLACE;
504 mods[i]->mod_type = strdup(nickserv_conf.ldap_field_email);
505 mods[i]->mod_values = email_vals;
506 i++;
507 }
508 mods[i] = NULL;
509 *num_mods_ret = num_mods;
510 return mods;
511 }
512
513
514 /* Save email or password to server
515 *
516 * password - UNENCRYPTED password. This function encrypts if libs are available
517 * email - email address
518 *
519 * NULL to make no change
520 */
521 int ldap_do_modify(const char *account, const char *password, const char *email)
522 {
523 char dn[MAXLEN];
524 LDAPMod **mods;
525 int rc, i;
526 int num_mods;
527 char *passbuf = NULL;
528
529 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
530 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
531 return rc;
532 }
533
534 if(password) {
535 passbuf = make_password(password);
536 }
537
538 snprintf(dn, MAXLEN-1, nickserv_conf.ldap_dn_fmt, account);
539 mods = make_mods_modify(passbuf, email, &num_mods);
540 if(!mods) {
541 log_module(MAIN_LOG, LOG_ERROR, "Error building mods for ldap_add");
542 return LDAP_OTHER;
543 }
544 rc = ldap_modify_s(ld, dn, mods);
545 if(rc != LDAP_SUCCESS) {
546 log_module(MAIN_LOG, LOG_ERROR, "Error adding ldap account: %s -- %s", account, ldap_err2string(rc));
547 return rc;
548 }
549 for(i = 0; i < num_mods; i++) {
550 free(mods[i]->mod_type);
551 free(mods[i]);
552 }
553 free(mods);
554 if(passbuf)
555 free(passbuf);
556 return rc;
557 }
558
559 LDAPMod **make_mods_group(const char *account, int operation, int *num_mods_ret)
560 {
561 static char *uid_vals[] = { NULL, NULL };
562 int num_mods = 1;
563 int i;
564 /* TODO: take this from nickserv_conf.ldap_add_objects */
565 LDAPMod **mods;
566
567 uid_vals[0] = (char *) account;
568
569 if(!(nickserv_conf.ldap_field_group_member && *nickserv_conf.ldap_field_group_member))
570 return 0; /* password required */
571
572 mods = ( LDAPMod ** ) malloc(( num_mods + 1 ) * sizeof( LDAPMod * ));
573 for( i = 0; i < num_mods; i++) {
574 mods[i] = (LDAPMod *) malloc(sizeof(LDAPMod));
575 memset(mods[i], 0, sizeof(LDAPMod));
576 }
577
578 i = 0;
579 mods[i]->mod_op = operation;
580 mods[i]->mod_type = strdup(nickserv_conf.ldap_field_group_member);
581 mods[i]->mod_values = uid_vals;
582 i++;
583 mods[i] = NULL;
584 *num_mods_ret = num_mods;
585 return mods;
586 }
587
588
589 int ldap_add2group(char *account, const char *group)
590 {
591 LDAPMod **mods;
592 int num_mods;
593 int rc, i;
594
595 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
596 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
597 return rc;
598 }
599 mods = make_mods_group(account, LDAP_MOD_ADD, &num_mods);
600 if(!mods) {
601 log_module(MAIN_LOG, LOG_ERROR, "Error building mods for add2group");
602 return LDAP_OTHER;
603 }
604 rc = ldap_modify_s(ld, group, mods);
605 if(rc != LDAP_SUCCESS && rc != LDAP_ALREADY_EXISTS) {
606 log_module(MAIN_LOG, LOG_ERROR, "Error adding %s to group %s: %s", account, group, ldap_err2string(rc));
607 return rc;
608 }
609 for(i = 0; i < num_mods; i++) {
610 free(mods[i]->mod_type);
611 free(mods[i]);
612 }
613 free(mods);
614 return rc;
615 }
616
617 int ldap_delfromgroup(char *account, const char *group)
618 {
619 LDAPMod **mods;
620 int num_mods;
621 int rc, i;
622
623 if(LDAP_SUCCESS != ( rc = ldap_do_admin_bind())) {
624 log_module(MAIN_LOG, LOG_ERROR, "failed to bind as admin");
625 return rc;
626 }
627 mods = make_mods_group(account, LDAP_MOD_DELETE, &num_mods);
628 if(!mods) {
629 log_module(MAIN_LOG, LOG_ERROR, "Error building mods for delfromgroup");
630 return LDAP_OTHER;
631 }
632 rc = ldap_modify_s(ld, group, mods);
633 if(rc != LDAP_SUCCESS && rc != LDAP_NO_SUCH_ATTRIBUTE) {
634 log_module(MAIN_LOG, LOG_ERROR, "Error removing %s from group %s: %s", account, group, ldap_err2string(rc));
635 return rc;
636 }
637 for(i = 0; i < num_mods; i++) {
638 free(mods[i]->mod_type);
639 free(mods[i]);
640 }
641 free(mods);
642 return rc;
643 }
644
645
646 void ldap_close()
647 {
648 ldap_unbind(ld);
649 }
650
651 #endif