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