xref: /openbsd-src/usr.bin/ldap/ldapclient.c (revision 46035553bfdd96e63c94e32da0210227ec2e3cf1)
1 /*	$OpenBSD: ldapclient.c,v 1.12 2019/01/26 10:58:54 deraadt Exp $	*/
2 
3 /*
4  * Copyright (c) 2018 Reyk Floeter <reyk@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/param.h>
20 #include <sys/queue.h>
21 #include <sys/socket.h>
22 #include <sys/stat.h>
23 #include <sys/tree.h>
24 #include <sys/un.h>
25 
26 #include <netinet/in.h>
27 #include <arpa/inet.h>
28 
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <stdint.h>
32 #include <unistd.h>
33 #include <ctype.h>
34 #include <err.h>
35 #include <errno.h>
36 #include <event.h>
37 #include <fcntl.h>
38 #include <limits.h>
39 #include <netdb.h>
40 #include <pwd.h>
41 #include <readpassphrase.h>
42 #include <resolv.h>
43 #include <signal.h>
44 #include <string.h>
45 #include <vis.h>
46 
47 #include "aldap.h"
48 #include "log.h"
49 
50 #define F_STARTTLS	0x01
51 #define F_TLS		0x02
52 #define F_NEEDAUTH	0x04
53 #define F_LDIF		0x08
54 
55 #define LDAPHOST	"localhost"
56 #define LDAPFILTER	"(objectClass=*)"
57 #define LDIF_LINELENGTH	79
58 #define LDAPPASSMAX	1024
59 
60 struct ldapc {
61 	struct aldap		*ldap_al;
62 	char			*ldap_host;
63 	int			 ldap_port;
64 	const char		*ldap_capath;
65 	char			*ldap_binddn;
66 	char			*ldap_secret;
67 	unsigned int		 ldap_flags;
68 	enum protocol_op	 ldap_req;
69 	enum aldap_protocol	 ldap_protocol;
70 	struct aldap_url	 ldap_url;
71 };
72 
73 struct ldapc_search {
74 	int			 ls_sizelimit;
75 	int			 ls_timelimit;
76 	char			*ls_basedn;
77 	char			*ls_filter;
78 	int			 ls_scope;
79 	char			**ls_attr;
80 };
81 
82 __dead void	 usage(void);
83 int		 ldapc_connect(struct ldapc *);
84 int		 ldapc_search(struct ldapc *, struct ldapc_search *);
85 int		 ldapc_printattr(struct ldapc *, const char *,
86 		    const struct ber_octetstring *);
87 void		 ldapc_disconnect(struct ldapc *);
88 int		 ldapc_parseurl(struct ldapc *, struct ldapc_search *,
89 		    const char *);
90 const char	*ldapc_resultcode(enum result_code);
91 const char	*url_decode(char *);
92 
93 __dead void
94 usage(void)
95 {
96 	extern char	*__progname;
97 
98 	fprintf(stderr,
99 "usage: %s search [-LvWxZ] [-b basedn] [-c CAfile] [-D binddn] [-H host]\n"
100 "	    [-l timelimit] [-s scope] [-w secret] [-y secretfile] [-z sizelimit]\n"
101 "	    [filter] [attributes ...]\n",
102 	    __progname);
103 
104 	exit(1);
105 }
106 
107 int
108 main(int argc, char *argv[])
109 {
110 	char			 passbuf[LDAPPASSMAX];
111 	const char		*errstr, *url = NULL, *secretfile = NULL;
112 	struct stat		 st;
113 	struct ldapc		 ldap;
114 	struct ldapc_search	 ls;
115 	int			 ch;
116 	int			 verbose = 1;
117 	FILE			*fp;
118 
119 	if (pledge("stdio inet unix tty rpath dns", NULL) == -1)
120 		err(1, "pledge");
121 
122 	log_init(verbose, 0);
123 
124 	memset(&ldap, 0, sizeof(ldap));
125 	memset(&ls, 0, sizeof(ls));
126 	ls.ls_scope = -1;
127 	ldap.ldap_port = -1;
128 
129 	/*
130 	 * Check the command.  Currently only "search" is supported but
131 	 * it could be extended with others such as add, modify, or delete.
132 	 */
133 	if (argc < 2)
134 		usage();
135 	else if (strcmp("search", argv[1]) == 0)
136 		ldap.ldap_req = LDAP_REQ_SEARCH;
137 	else
138 		usage();
139 	argc--;
140 	argv++;
141 
142 	while ((ch = getopt(argc, argv, "b:c:D:H:Ll:s:vWw:xy:Zz:")) != -1) {
143 		switch (ch) {
144 		case 'b':
145 			ls.ls_basedn = optarg;
146 			break;
147 		case 'c':
148 			ldap.ldap_capath = optarg;
149 			break;
150 		case 'D':
151 			ldap.ldap_binddn = optarg;
152 			ldap.ldap_flags |= F_NEEDAUTH;
153 			break;
154 		case 'H':
155 			url = optarg;
156 			break;
157 		case 'L':
158 			ldap.ldap_flags |= F_LDIF;
159 			break;
160 		case 'l':
161 			ls.ls_timelimit = strtonum(optarg, 0, INT_MAX,
162 			    &errstr);
163 			if (errstr != NULL)
164 				errx(1, "timelimit %s", errstr);
165 			break;
166 		case 's':
167 			if (strcasecmp("base", optarg) == 0)
168 				ls.ls_scope = LDAP_SCOPE_BASE;
169 			else if (strcasecmp("one", optarg) == 0)
170 				ls.ls_scope = LDAP_SCOPE_ONELEVEL;
171 			else if (strcasecmp("sub", optarg) == 0)
172 				ls.ls_scope = LDAP_SCOPE_SUBTREE;
173 			else
174 				errx(1, "invalid scope: %s", optarg);
175 			break;
176 		case 'v':
177 			verbose++;
178 			break;
179 		case 'w':
180 			ldap.ldap_secret = optarg;
181 			ldap.ldap_flags |= F_NEEDAUTH;
182 			break;
183 		case 'W':
184 			ldap.ldap_flags |= F_NEEDAUTH;
185 			break;
186 		case 'x':
187 			/* provided for compatibility */
188 			break;
189 		case 'y':
190 			secretfile = optarg;
191 			ldap.ldap_flags |= F_NEEDAUTH;
192 			break;
193 		case 'Z':
194 			ldap.ldap_flags |= F_STARTTLS;
195 			break;
196 		case 'z':
197 			ls.ls_sizelimit = strtonum(optarg, 0, INT_MAX,
198 			    &errstr);
199 			if (errstr != NULL)
200 				errx(1, "sizelimit %s", errstr);
201 			break;
202 		default:
203 			usage();
204 		}
205 	}
206 	argc -= optind;
207 	argv += optind;
208 
209 	log_setverbose(verbose);
210 
211 	if (url != NULL && ldapc_parseurl(&ldap, &ls, url) == -1)
212 		errx(1, "ldapurl");
213 
214 	/* Set the default after parsing URL and/or options */
215 	if (ldap.ldap_host == NULL)
216 		ldap.ldap_host = LDAPHOST;
217 	if (ldap.ldap_port == -1)
218 		ldap.ldap_port = ldap.ldap_protocol == LDAPS ?
219 		    LDAPS_PORT : LDAP_PORT;
220 	if (ldap.ldap_protocol == LDAP && (ldap.ldap_flags & F_STARTTLS))
221 		ldap.ldap_protocol = LDAPTLS;
222 	if (ldap.ldap_capath == NULL)
223 		ldap.ldap_capath = tls_default_ca_cert_file();
224 	if (ls.ls_basedn == NULL)
225 		ls.ls_basedn = "";
226 	if (ls.ls_scope == -1)
227 		ls.ls_scope = LDAP_SCOPE_SUBTREE;
228 	if (ls.ls_filter == NULL)
229 		ls.ls_filter = LDAPFILTER;
230 
231 	if (ldap.ldap_flags & F_NEEDAUTH) {
232 		if (ldap.ldap_binddn == NULL) {
233 			log_warnx("missing -D binddn");
234 			usage();
235 		}
236 		if (secretfile != NULL) {
237 			if (ldap.ldap_secret != NULL)
238 				errx(1, "conflicting -w/-y options");
239 
240 			/* read password from stdin or file (first line) */
241 			if (strcmp(secretfile, "-") == 0)
242 				fp = stdin;
243 			else if (stat(secretfile, &st) == -1)
244 				err(1, "failed to access %s", secretfile);
245 			else if (S_ISREG(st.st_mode) && (st.st_mode & S_IROTH))
246 				errx(1, "%s is world-readable", secretfile);
247 			else if ((fp = fopen(secretfile, "r")) == NULL)
248 				err(1, "failed to open %s", secretfile);
249 			if (fgets(passbuf, sizeof(passbuf), fp) == NULL)
250 				err(1, "failed to read %s", secretfile);
251 			if (fp != stdin)
252 				fclose(fp);
253 
254 			passbuf[strcspn(passbuf, "\n")] = '\0';
255 			ldap.ldap_secret = passbuf;
256 		}
257 		if (ldap.ldap_secret == NULL) {
258 			if (readpassphrase("Password: ",
259 			    passbuf, sizeof(passbuf), RPP_REQUIRE_TTY) == NULL)
260 				errx(1, "failed to read LDAP password");
261 			ldap.ldap_secret = passbuf;
262 		}
263 	}
264 
265 	if (pledge("stdio inet unix rpath dns", NULL) == -1)
266 		err(1, "pledge");
267 
268 	/* optional search filter */
269 	if (argc && strchr(argv[0], '=') != NULL) {
270 		ls.ls_filter = argv[0];
271 		argc--;
272 		argv++;
273 	}
274 	/* search attributes */
275 	if (argc)
276 		ls.ls_attr = argv;
277 
278 	if (ldapc_connect(&ldap) == -1)
279 		errx(1, "LDAP connection failed");
280 
281 	if (pledge("stdio", NULL) == -1)
282 		err(1, "pledge");
283 
284 	if (ldapc_search(&ldap, &ls) == -1)
285 		errx(1, "LDAP search failed");
286 
287 	ldapc_disconnect(&ldap);
288 	aldap_free_url(&ldap.ldap_url);
289 
290 	return (0);
291 }
292 
293 int
294 ldapc_search(struct ldapc *ldap, struct ldapc_search *ls)
295 {
296 	struct aldap_page_control	*pg = NULL;
297 	struct aldap_message		*m;
298 	const char			*errstr;
299 	const char			*searchdn, *dn = NULL;
300 	char				*outkey;
301 	struct aldap_stringset		*outvalues;
302 	int				 ret, code, fail = 0;
303 	size_t				 i;
304 
305 	if (ldap->ldap_flags & F_LDIF)
306 		printf("version: 1\n");
307 	do {
308 		if (aldap_search(ldap->ldap_al, ls->ls_basedn, ls->ls_scope,
309 		    ls->ls_filter, ls->ls_attr, 0, ls->ls_sizelimit,
310 		    ls->ls_timelimit, pg) == -1) {
311 			aldap_get_errno(ldap->ldap_al, &errstr);
312 			log_warnx("LDAP search failed: %s", errstr);
313 			return (-1);
314 		}
315 
316 		if (pg != NULL) {
317 			aldap_freepage(pg);
318 			pg = NULL;
319 		}
320 
321 		while ((m = aldap_parse(ldap->ldap_al)) != NULL) {
322 			if (ldap->ldap_al->msgid != m->msgid) {
323 				goto fail;
324 			}
325 
326 			if ((code = aldap_get_resultcode(m)) != LDAP_SUCCESS) {
327 				log_warnx("LDAP search failed: %s(%d)",
328 				    ldapc_resultcode(code), code);
329 				break;
330 			}
331 
332 			if (m->message_type == LDAP_RES_SEARCH_RESULT) {
333 				if (m->page != NULL && m->page->cookie_len != 0)
334 					pg = m->page;
335 				else
336 					pg = NULL;
337 
338 				aldap_freemsg(m);
339 				break;
340 			}
341 
342 			if (m->message_type != LDAP_RES_SEARCH_ENTRY) {
343 				goto fail;
344 			}
345 
346 			if (aldap_count_attrs(m) < 1) {
347 				aldap_freemsg(m);
348 				continue;
349 			}
350 
351 			if ((searchdn = aldap_get_dn(m)) == NULL)
352 				goto fail;
353 
354 			if (dn != NULL)
355 				printf("\n");
356 			else
357 				dn = ls->ls_basedn;
358 			if (strcmp(dn, searchdn) != 0)
359 				printf("dn: %s\n", searchdn);
360 
361 			for (ret = aldap_first_attr(m, &outkey, &outvalues);
362 			    ret != -1;
363 			    ret = aldap_next_attr(m, &outkey, &outvalues)) {
364 				for (i = 0; i < outvalues->len; i++) {
365 					if (ldapc_printattr(ldap, outkey,
366 					    &(outvalues->str[i])) == -1) {
367 						fail = 1;
368 						break;
369 					}
370 				}
371 			}
372 			free(outkey);
373 			aldap_free_attr(outvalues);
374 
375 			aldap_freemsg(m);
376 		}
377 	} while (pg != NULL && fail == 0);
378 
379 	if (fail)
380 		return (-1);
381 	return (0);
382  fail:
383 	ldapc_disconnect(ldap);
384 	return (-1);
385 }
386 
387 int
388 ldapc_printattr(struct ldapc *ldap, const char *key,
389     const struct ber_octetstring *value)
390 {
391 	char			*p = NULL, *out;
392 	const unsigned char	*cp;
393 	int			 encode;
394 	size_t			 i, inlen, outlen, left;
395 
396 	if (ldap->ldap_flags & F_LDIF) {
397 		/* OpenLDAP encodes the userPassword by default */
398 		if (strcasecmp("userPassword", key) == 0)
399 			encode = 1;
400 		else
401 			encode = 0;
402 
403 		/*
404 		 * The LDIF format a set of characters that can be included
405 		 * in SAFE-STRINGs. String value that do not match the
406 		 * criteria must be encoded as Base64.
407 		 */
408 		cp = (const unsigned char *)value->ostr_val;
409 		/* !SAFE-INIT-CHAR: SAFE-CHAR minus %x20 %x3A %x3C */
410 		if (*cp == ' ' ||
411 		    *cp == ':' ||
412 		    *cp == '<')
413 			encode = 1;
414 		for (i = 0; encode == 0 && i < value->ostr_len - 1; i++) {
415 			/* !SAFE-CHAR %x01-09 / %x0B-0C / %x0E-7F */
416 			if (cp[i] > 127 ||
417 			    cp[i] == '\0' ||
418 			    cp[i] == '\n' ||
419 			    cp[i] == '\r')
420 				encode = 1;
421 		}
422 
423 		if (!encode) {
424 			if (asprintf(&p, "%s: %s", key,
425 			    (const char *)value->ostr_val) == -1) {
426 				log_warnx("asprintf");
427 				return (-1);
428 			}
429 		} else {
430 			outlen = (((value->ostr_len + 2) / 3) * 4) + 1;
431 
432 			if ((out = calloc(1, outlen)) == NULL ||
433 			    b64_ntop(value->ostr_val, value->ostr_len, out,
434 			    outlen) == -1) {
435 				log_warnx("Base64 encoding failed");
436 				free(p);
437 				return (-1);
438 			}
439 
440 			/* Base64 is indicated with a double-colon */
441 			if (asprintf(&p, "%s:: %s", key, out) == -1) {
442 				log_warnx("asprintf");
443 				free(out);
444 				return (-1);
445 			}
446 			free(out);
447 		}
448 
449 		/* Wrap lines */
450 		for (outlen = 0, inlen = strlen(p);
451 		    outlen < inlen;
452 		    outlen += LDIF_LINELENGTH - 1) {
453 			if (outlen)
454 				putchar(' ');
455 			if (outlen > LDIF_LINELENGTH)
456 				outlen--;
457 			/* max. line length - newline - optional indent */
458 			left = MIN(inlen - outlen, outlen ?
459 			    LDIF_LINELENGTH - 2 :
460 			    LDIF_LINELENGTH - 1);
461 			fwrite(p + outlen, left, 1, stdout);
462 			putchar('\n');
463 		}
464 	} else {
465 		/*
466 		 * Use vis(1) instead of base64 encoding of non-printable
467 		 * values.  This is much nicer as it always prdocues a
468 		 * human-readable visual output.  This can safely be done
469 		 * on all values no matter if they include non-printable
470 		 * characters.
471 		 */
472 		p = calloc(1, 4 * value->ostr_len + 1);
473 		if (strvisx(p, value->ostr_val, value->ostr_len,
474 		    VIS_SAFE|VIS_NL) == -1) {
475 			log_warn("visual encoding failed");
476 			return (-1);
477 		}
478 
479 		printf("%s: %s\n", key, p);
480 	}
481 
482 	free(p);
483 	return (0);
484 }
485 
486 int
487 ldapc_connect(struct ldapc *ldap)
488 {
489 	struct addrinfo		 ai, *res, *res0;
490 	struct sockaddr_un	 un;
491 	int			 ret = -1, saved_errno, fd = -1, code;
492 	struct aldap_message	*m;
493 	const char		*errstr;
494 	struct tls_config	*tls_config;
495 	char			 port[6];
496 
497 	if (ldap->ldap_protocol == LDAPI) {
498 		memset(&un, 0, sizeof(un));
499 		un.sun_family = AF_UNIX;
500 		if (strlcpy(un.sun_path, ldap->ldap_host,
501 		    sizeof(un.sun_path)) >= sizeof(un.sun_path)) {
502 			log_warnx("socket '%s' too long", ldap->ldap_host);
503 			goto done;
504 		}
505 		if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1 ||
506 		    connect(fd, (struct sockaddr *)&un, sizeof(un)) == -1)
507 			goto done;
508 		goto init;
509 	}
510 
511 	memset(&ai, 0, sizeof(ai));
512 	ai.ai_family = AF_UNSPEC;
513 	ai.ai_socktype = SOCK_STREAM;
514 	ai.ai_protocol = IPPROTO_TCP;
515 	(void)snprintf(port, sizeof(port), "%u", ldap->ldap_port);
516 	if ((code = getaddrinfo(ldap->ldap_host, port,
517 	    &ai, &res0)) != 0) {
518 		log_warnx("%s", gai_strerror(code));
519 		goto done;
520 	}
521 	for (res = res0; res; res = res->ai_next, fd = -1) {
522 		if ((fd = socket(res->ai_family, res->ai_socktype,
523 		    res->ai_protocol)) == -1)
524 			continue;
525 
526 		if (connect(fd, res->ai_addr, res->ai_addrlen) >= 0)
527 			break;
528 
529 		saved_errno = errno;
530 		close(fd);
531 		errno = saved_errno;
532 	}
533 	freeaddrinfo(res0);
534 	if (fd == -1)
535 		goto done;
536 
537  init:
538 	if ((ldap->ldap_al = aldap_init(fd)) == NULL) {
539 		warn("LDAP init failed");
540 		close(fd);
541 		goto done;
542 	}
543 
544 	if (ldap->ldap_flags & F_STARTTLS) {
545 		log_debug("%s: requesting STARTTLS", __func__);
546 		if (aldap_req_starttls(ldap->ldap_al) == -1) {
547 			log_warnx("failed to request STARTTLS");
548 			goto done;
549 		}
550 
551 		if ((m = aldap_parse(ldap->ldap_al)) == NULL) {
552 			log_warnx("failed to parse STARTTLS response");
553 			goto done;
554 		}
555 
556 		if (ldap->ldap_al->msgid != m->msgid ||
557 		    (code = aldap_get_resultcode(m)) != LDAP_SUCCESS) {
558 			log_warnx("STARTTLS failed: %s(%d)",
559 			    ldapc_resultcode(code), code);
560 			aldap_freemsg(m);
561 			goto done;
562 		}
563 		aldap_freemsg(m);
564 	}
565 
566 	if (ldap->ldap_flags & (F_STARTTLS | F_TLS)) {
567 		log_debug("%s: starting TLS", __func__);
568 
569 		if ((tls_config = tls_config_new()) == NULL) {
570 			log_warnx("TLS config failed");
571 			goto done;
572 		}
573 
574 		if (tls_config_set_ca_file(tls_config,
575 		    ldap->ldap_capath) == -1) {
576 			log_warnx("unable to set CA %s", ldap->ldap_capath);
577 			goto done;
578 		}
579 
580 		if (aldap_tls(ldap->ldap_al, tls_config, ldap->ldap_host) < 0) {
581 			aldap_get_errno(ldap->ldap_al, &errstr);
582 			log_warnx("TLS failed: %s", errstr);
583 			goto done;
584 		}
585 	}
586 
587 	if (ldap->ldap_flags & F_NEEDAUTH) {
588 		log_debug("%s: bind request", __func__);
589 		if (aldap_bind(ldap->ldap_al, ldap->ldap_binddn,
590 		    ldap->ldap_secret) == -1) {
591 			log_warnx("bind request failed");
592 			goto done;
593 		}
594 
595 		if ((m = aldap_parse(ldap->ldap_al)) == NULL) {
596 			log_warnx("failed to parse bind response");
597 			goto done;
598 		}
599 
600 		if (ldap->ldap_al->msgid != m->msgid ||
601 		    (code = aldap_get_resultcode(m)) != LDAP_SUCCESS) {
602 			log_warnx("bind failed: %s(%d)",
603 			    ldapc_resultcode(code), code);
604 			aldap_freemsg(m);
605 			goto done;
606 		}
607 		aldap_freemsg(m);
608 	}
609 
610 	log_debug("%s: connected", __func__);
611 
612 	ret = 0;
613  done:
614 	if (ret != 0)
615 		ldapc_disconnect(ldap);
616 	if (ldap->ldap_secret != NULL)
617 		explicit_bzero(ldap->ldap_secret,
618 		    strlen(ldap->ldap_secret));
619 	return (ret);
620 }
621 
622 void
623 ldapc_disconnect(struct ldapc *ldap)
624 {
625 	if (ldap->ldap_al == NULL)
626 		return;
627 	aldap_close(ldap->ldap_al);
628 	ldap->ldap_al = NULL;
629 }
630 
631 const char *
632 ldapc_resultcode(enum result_code code)
633 {
634 #define CODE(_X)	case _X:return (#_X)
635 	switch (code) {
636 	CODE(LDAP_SUCCESS);
637 	CODE(LDAP_OPERATIONS_ERROR);
638 	CODE(LDAP_PROTOCOL_ERROR);
639 	CODE(LDAP_TIMELIMIT_EXCEEDED);
640 	CODE(LDAP_SIZELIMIT_EXCEEDED);
641 	CODE(LDAP_COMPARE_FALSE);
642 	CODE(LDAP_COMPARE_TRUE);
643 	CODE(LDAP_STRONG_AUTH_NOT_SUPPORTED);
644 	CODE(LDAP_STRONG_AUTH_REQUIRED);
645 	CODE(LDAP_REFERRAL);
646 	CODE(LDAP_ADMINLIMIT_EXCEEDED);
647 	CODE(LDAP_UNAVAILABLE_CRITICAL_EXTENSION);
648 	CODE(LDAP_CONFIDENTIALITY_REQUIRED);
649 	CODE(LDAP_SASL_BIND_IN_PROGRESS);
650 	CODE(LDAP_NO_SUCH_ATTRIBUTE);
651 	CODE(LDAP_UNDEFINED_TYPE);
652 	CODE(LDAP_INAPPROPRIATE_MATCHING);
653 	CODE(LDAP_CONSTRAINT_VIOLATION);
654 	CODE(LDAP_TYPE_OR_VALUE_EXISTS);
655 	CODE(LDAP_INVALID_SYNTAX);
656 	CODE(LDAP_NO_SUCH_OBJECT);
657 	CODE(LDAP_ALIAS_PROBLEM);
658 	CODE(LDAP_INVALID_DN_SYNTAX);
659 	CODE(LDAP_ALIAS_DEREF_PROBLEM);
660 	CODE(LDAP_INAPPROPRIATE_AUTH);
661 	CODE(LDAP_INVALID_CREDENTIALS);
662 	CODE(LDAP_INSUFFICIENT_ACCESS);
663 	CODE(LDAP_BUSY);
664 	CODE(LDAP_UNAVAILABLE);
665 	CODE(LDAP_UNWILLING_TO_PERFORM);
666 	CODE(LDAP_LOOP_DETECT);
667 	CODE(LDAP_NAMING_VIOLATION);
668 	CODE(LDAP_OBJECT_CLASS_VIOLATION);
669 	CODE(LDAP_NOT_ALLOWED_ON_NONLEAF);
670 	CODE(LDAP_NOT_ALLOWED_ON_RDN);
671 	CODE(LDAP_ALREADY_EXISTS);
672 	CODE(LDAP_NO_OBJECT_CLASS_MODS);
673 	CODE(LDAP_AFFECTS_MULTIPLE_DSAS);
674 	CODE(LDAP_OTHER);
675 	default:
676 		return ("UNKNOWN_ERROR");
677 	}
678 };
679 
680 int
681 ldapc_parseurl(struct ldapc *ldap, struct ldapc_search *ls, const char *url)
682 {
683 	struct aldap_url	*lu = &ldap->ldap_url;
684 	size_t			 i;
685 
686 	memset(lu, 0, sizeof(*lu));
687 	lu->scope = -1;
688 
689 	if (aldap_parse_url(url, lu) == -1) {
690 		log_warnx("failed to parse LDAP URL");
691 		return (-1);
692 	}
693 
694 	/* The protocol part is optional and we default to ldap:// */
695 	if (lu->protocol == -1)
696 		lu->protocol = LDAP;
697 	else if (lu->protocol == LDAPI) {
698 		if (lu->port != 0 ||
699 		    url_decode(lu->host) == NULL) {
700 			log_warnx("invalid ldapi:// URL");
701 			return (-1);
702 		}
703 	} else if ((ldap->ldap_flags & F_STARTTLS) &&
704 	    lu->protocol != LDAPTLS) {
705 		log_warnx("conflicting protocol arguments");
706 		return (-1);
707 	} else if (lu->protocol == LDAPTLS)
708 		ldap->ldap_flags |= F_TLS|F_STARTTLS;
709 	else if (lu->protocol == LDAPS)
710 		ldap->ldap_flags |= F_TLS;
711 	ldap->ldap_protocol = lu->protocol;
712 
713 	ldap->ldap_host = lu->host;
714 	if (lu->port)
715 		ldap->ldap_port = lu->port;
716 
717 	/* The distinguished name has to be URL-encoded */
718 	if (lu->dn != NULL && ls->ls_basedn != NULL &&
719 	    strcasecmp(ls->ls_basedn, lu->dn) != 0) {
720 		log_warnx("conflicting basedn arguments");
721 		return (-1);
722 	}
723 	if (lu->dn != NULL) {
724 		if (url_decode(lu->dn) == NULL)
725 			return (-1);
726 		ls->ls_basedn = lu->dn;
727 	}
728 
729 	if (lu->scope != -1) {
730 		if (ls->ls_scope != -1 && (ls->ls_scope != lu->scope)) {
731 			log_warnx("conflicting scope arguments");
732 			return (-1);
733 		}
734 		ls->ls_scope = lu->scope;
735 	}
736 
737 	/* URL-decode optional attributes and the search filter */
738 	if (lu->attributes[0] != NULL) {
739 		for (i = 0; i < MAXATTR && lu->attributes[i] != NULL; i++)
740 			if (url_decode(lu->attributes[i]) == NULL)
741 				return (-1);
742 		ls->ls_attr = lu->attributes;
743 	}
744 	if (lu->filter != NULL) {
745 		if (url_decode(lu->filter) == NULL)
746 			return (-1);
747 		ls->ls_filter = lu->filter;
748 	}
749 
750 	return (0);
751 }
752 
753 /* From usr.sbin/httpd/httpd.c */
754 const char *
755 url_decode(char *url)
756 {
757 	char		*p, *q;
758 	char		 hex[3];
759 	unsigned long	 x;
760 
761 	hex[2] = '\0';
762 	p = q = url;
763 
764 	while (*p != '\0') {
765 		switch (*p) {
766 		case '%':
767 			/* Encoding character is followed by two hex chars */
768 			if (!(isxdigit((unsigned char)p[1]) &&
769 			    isxdigit((unsigned char)p[2])))
770 				return (NULL);
771 
772 			hex[0] = p[1];
773 			hex[1] = p[2];
774 
775 			/*
776 			 * We don't have to validate "hex" because it is
777 			 * guaranteed to include two hex chars followed by nul.
778 			 */
779 			x = strtoul(hex, NULL, 16);
780 			*q = (char)x;
781 			p += 2;
782 			break;
783 		default:
784 			*q = *p;
785 			break;
786 		}
787 		p++;
788 		q++;
789 	}
790 	*q = '\0';
791 
792 	return (url);
793 }
794