/*
 * RADLDAP - OpenRADIUS LDAP module
 *
 * This module has two modes of operation, depending on its command line
 * arguments. If you specify a bind DN and password, the module will perform
 * an LDAP bind at startup using those credentials.
 *
 * If you don't include these on the command line, the module will do its
 * bind operation each time it gets a request, taking the DN from the last
 * 'User-Name' attribute and using 'User-Password' as the password.
 *
 * The latter mode can only be used with PAP, but provides the easiest way to
 * make authentication work the same way as when using an LDAP client.
 *
 * In either case, the module performs a subtree search for each request, 
 * using the first 'str' as the base DN and the second 'str' as the search
 * filter.
 *
 * It then translates each LDAP attribute that is present in the (all!) 
 * objects returned by the search, to OpenRADIUS' space/vendor/attribute 
 * combination that is specified in the mapping file. The mapping file can 
 * be specified on the command line, otherwise a default filename is used.
 *
 * It sets the last instance of the 'int' attribute of its response to the 
 * number of objects returned by the search. 
 *
 * If you specify the '-u' command line switch, the module will unbind()
 * from the LDAP after each request, to work around buggy and leaking
 * LDAP implementations. This may useful especially if you use the
 * '(re-)bind using User-Name / User-Password' mode. LDAPv3 implementations
 * should support multiple binds during the same session fine, though.
 *
 * Author:
 * Emile van Bergen, emile@evbergen.xs4all.nl
 *
 * Permission to redistribute an original or modified version of this program
 * in source, intermediate or object code form is hereby granted exclusively
 * under the terms of the GNU General Public License, version 2. Please see the
 * file COPYING for details, or refer to http://www.gnu.org/copyleft/gpl.html.
 *
 * History:
 * 2001/11/28 - EvB - Created
 * 2002/03/19 - EvB - Made radldap.attrmap's path relative, which is possible
 * 		      now the server initialises the cwd for each subprocess
 * 		      according to the module base directory. Makes it easier
 * 		      to use different installations without editing much.
 * 2002/06/26 - EvB - Fixed trivial compiler warning
 * 2002/10/02 - EvB - Added missing sys/time.h
 * 2003/06/06 - EvB - Added -2 flag to prevent binding as version 3, which is
 *		      now the default if the client library supports it
 * 2005/06/21 - EvB - Set module interface version to 2
 * 2007/03/24 - EvB - Cleaned up dn-as-pseudo-attr code; even contained a
 * 		      buffer overflow that could be triggered by long
 * 		      DN's in the LDAP database when using that feature!
 */

char radldap_id[] = "RADLDAP - Copyright (C) 2001 Emile van Bergen.";


/*
 * INCLUDES & DEFINES
 */


#include <sys/types.h>	/* For u(_)int32_t, htonl() */
#include <sys/socket.h>	/* For u(_)int32_t, htonl() */
#include <netinet/in.h>	/* For u(_)int32_t, htonl() */

#include <sys/time.h>	/* For struct timeval on some systems */
#include <time.h>	/* For struct timeval on some systems */

#include <stdio.h>	/* For fprintf() */
#include <signal.h>	/* For signal() to ignore SIGPIPE during LDAP unbind */
#include <unistd.h>	/* For getopt() on some systems */
#include <stdlib.h>	/* For getopt() on others */
#include <string.h>	/* For strchr(), strtok(), strdup() */
#include <errno.h>	/* For errno */

#include <ldap.h>	/* The LDAP library */

#include <constants.h>	/* Server constants */

#include <platform.h>	/* For U_INT32_T */


#define RADLDAP_VERSION	"v0.9"


#ifndef RADLDAP_MAPFILE
#define RADLDAP_MAPFILE	"modules/radldap.attrmap"
#endif

#define SEARCH_TIMEOUT	15		/* Search timeout - should be shorter
					   than what's in 'configuration' */

#define MAPF_MAXENTRIES	128		/* Max. entries in mapping file */
#define MAPF_LINELEN	512		/* Max. mapping file line length */
#define LDAP_ATRNAMELEN	256		/* Max. length for LDAP attr. names */


/*
 * TYPES
 */


typedef struct atrmap {
	char *ldap_atrs[MAPF_MAXENTRIES + 1];	/* Zero terminated */
	U_INT32_T spcs[MAPF_MAXENTRIES];
	U_INT32_T vnds[MAPF_MAXENTRIES];
	U_INT32_T atrs[MAPF_MAXENTRIES];
	int entries;
} ATRMAP;


/* 
 * GLOBALS
 */


/* Command line options */

static char *mapfile, *binddn, *password, *host;
static int port, unbind_after_req, no_ldapv3, debug;


/*
 * FUNCTIONS
 */


void usage()
{
	fprintf(stderr, 
"Usage: radldap [-m mapfile ][-b binddn [-s password ]][-p port ]	\\\n"
"		[-u ][-d ]host\n"
"       radldap -v\n"
"       radldap -h\n");
	_exit(1);
}


/*
 * Parse command line options and set some global variables
 */


void parseoptions(int argc, char **argv)
{
	int c;

	/* Defaults */
	host = 0; mapfile = RADLDAP_MAPFILE; 
	binddn = 0; password = "";
	port = LDAP_PORT; unbind_after_req = no_ldapv3 = debug = 0;

	/* Handle options */
	while((c = getopt(argc, argv, "m:b:s:p:u2dhv")) != -1) {
		switch(c) {
		  case 'm': mapfile = optarg; break;
		  case 'b': binddn = optarg; break;
		  case 's': password = optarg; break;
		  case 'p':
			port = atoi(optarg); 
			if (!port) { 
				fprintf(stderr, "radldap: Invalid port!\n\n");
				usage(); 
			}
			break;
		  case 'u': unbind_after_req++; break;
		  case '2': no_ldapv3++; break;
		  case 'd': debug++; break;
		  case 'v':
			fprintf(stderr, "\nRadLDAP module " RADLDAP_VERSION ". "
			   "Copyright (C) 2001 Emile van Bergen / E-Advies.\n\n"
"Permission to redistribute an original or modified version of this program\n"
"in source, intermediate or object code form is hereby granted exclusively\n"
"under the terms of the GNU General Public License, version 2. Please see the\n"
"file COPYING for details, or refer to http://www.gnu.org/copyleft/gpl.html.\n"
"\nDefault attribute mapfile: " RADLDAP_MAPFILE "\n\n");
		  case 'h':
		  case '?':
			usage();
		}
	}

	/* Handle positional argument */
	if (optind < argc - 1) { 
		fprintf(stderr, "radldap: Too many arguments!\n\n"); usage(); 
	}
	if (optind > argc - 1) { 
		fprintf(stderr, "radldap: Not enough arguments!\n\n"); usage();
	}
	host = argv[optind];
	if (!host || !host[0]) { 
		fprintf(stderr, "radldap: Invalid host!\n\n"); usage(); 
	}
}


/*
 * Setup attribute mapping table according to file named by 'mapfile'.
 */


ATRMAP *setup_atrmap()
{
	FILE *f;
	static char line[MAPF_LINELEN + 2];
	ATRMAP *ret;
	char *name, *s;

	if (!(f = fopen(mapfile, "r"))) {
		fprintf(stderr, "radldap: Could not open '%s': %s!\n", 
			mapfile, strerror(errno));
		_exit(4);
	}

	/* Allocate map */
	ret = (ATRMAP *)malloc(sizeof(ATRMAP));
	if (!ret) { perror("radldap: No memory for mapfile"); _exit(4); }
	memset(ret, 0, sizeof(ATRMAP));

	/* Get a line */
	while(fgets(line, MAPF_LINELEN, f)) {

		/* Remove comments */
		if ((s = strchr(line, '#'))) *s = 0;

		/* Get first token, skip empty lines */
		name = strtok(line, " \t\n\r"); if (!name) continue;
		ret->ldap_atrs[ret->entries] = strdup(name);
		ret->vnds[ret->entries] = C_VND_ANY;

		/* Get other 3 tokens, convert to numeric and check syntax */
		if (!(s = strtok(0, " \t\n\r"))	||
		    (ret->spcs[ret->entries] = strtoul(s, &s, 10), s && *s) ||
		    !(s = strtok(0, " \t\n\r"))	||
		    (*s != 'x' &&
		     (ret->vnds[ret->entries] = strtoul(s, &s, 10), s && *s)) ||
		    !(s = strtok(0, " \t\n\r"))	||
		    (ret->atrs[ret->entries] = strtoul(s, &s, 10), s && *s)) {

			fprintf(stderr, "radldap: Missing or invalid space, "
					"vendor or attribute for '%s' in map"
					"file '%s'!\n", name, mapfile);
			_exit(4);
		}

		/* Bump entry count */
		if (ret->entries++ == MAPF_MAXENTRIES) {
			fprintf(stderr, "radldap: Exceeded maximum mapfile "
					"entries (%d)!\n", MAPF_MAXENTRIES);
			_exit(4);
		}
	}
	fclose(f);

	return ret;
}


/*
 * Wrapper around ldap_simple_bind_s, with debugging
 */


int do_bind(LDAP *ld, char *dn, char *pass)
{
	if (debug) {
		if (pass[0]) {
			fprintf(stderr, "radldap: Binding on '%s'\n"
					"     using password '%s'\n",
				dn, debug > 1 ? pass : "*******");
		}
		else {
			fprintf(stderr, "radldap: Binding anonymously\n");
		}
	}
	return ldap_simple_bind_s(ld, dn, pass);
}


/*
 * Setup LDAP association by performing ldap_init and optionally binding
 * to the dn given after the '-b' command line argument if specified
 * (i.e. not-NULL). If binddn is not-NULL but an empty string, the bind is 
 * still done but treated by LDAP as anonymous.
 *
 * If the bind fails, this routine does not return but calls _exit().
 * This gives a better recovery chance in case of bugs; OpenRADIUS will
 * restart the module itself.
 */


LDAP *setup_ldap()
{
	LDAP *ret;
	int n;

	if (debug) fprintf(stderr, "radldap: Setting up LDAP for %s, port %d\n",
			   host, port);
	if (!(ret = ldap_init(host, port))) {
		fprintf(stderr, "radldap: Could not initialize LDAP!\n");
		_exit(2); 
	}

	/* Set protocol version to 3 if the client library supports it and
	   the user hasn't asked not to do it */
#if defined(LDAP_OPT_PROTOCOL_VERSION) && defined(LDAP_VERSION3)
	if (!no_ldapv3) {
		n = LDAP_VERSION3;
		ldap_set_option(ret, LDAP_OPT_PROTOCOL_VERSION, &n);
	}
#endif

	/* Bind if we were requested to do so */
	if (binddn && do_bind(ret, binddn, password)) {
		ldap_perror(ret, "radldap: Could not bind");
		_exit(3);
	}

	return ret;
}


/*
 * Set response message to contain only a single 'int' with value zero.
 */


U_INT32_T set_nak(U_INT32_T *msgbuf)
{
	msgbuf[1] = htonl(28);
	msgbuf[2] = htonl(C_DS_INTERNAL); 
	msgbuf[3] = htonl(C_VND_ANY);
	msgbuf[4] = htonl(C_DI_INT); 
	msgbuf[5] = htonl(4);
	msgbuf[6] = 0;

	return 7 << 2;
}


/* adds value for an LDAP attribute, given by mapidx; outcntleft and returned
   length are in 32 bit words */

unsigned add_val(U_INT32_T *out, int outcnt, ATRMAP *map, int mapidx, 
		 struct berval *val)
{
	U_INT32_T *o = out;

	unsigned long len = val->bv_len;

	if (debug) {
		fprintf(stderr, "             %.*s (len %lu)\n", 
			(unsigned int)len, val->bv_val, len);
	}
	
	/* Check if there's room */

	if (outcnt < 4 + ((len + 3) >> 2)) {
		fprintf(stderr, "radldap: No more room for "
				"attribute '%s', len %lu: %d bytes left!\n",
			map->ldap_atrs[mapidx], len, outcnt << 2);
		return 0;
	}

	/* Write attribute to message buffer and advance ptr */

	*o++ = htonl(map->spcs[mapidx]);
	*o++ = htonl(map->vnds[mapidx]);
	*o++ = htonl(map->atrs[mapidx]);
	*o++ = htonl(len);
	memcpy(o, val->bv_val, len);
	o += 4 + ((len + 3) >> 2);

	return o - out;
}


/*
 * Set response message according to the LDAP search we perform and
 * return the number of objects retrieved or < 0 if error. The buffer
 * passed in 'msgbuf' must be C_MAX_MSGSIZE long; the length of the
 * response is returned in the uint32 pointed to by n.
 */


int do_search(LDAP *ld, char *basedn, char *filter, 
	      ATRMAP *map, U_INT32_T *msgbuf, U_INT32_T *len)
{
	struct timeval tv;
	LDAPMessage *res, *ent;
	struct berval **vals, **v, bv;
	U_INT32_T *o, *e;
	char *s;
	int n, retcnt;

	if (debug) fprintf(stderr, "radldap: Searching using base dn '%s',\n"
				   "         filter '%s'\n", 
			   basedn, filter);

	/* Set output pointer and end of buffer */

	o = msgbuf + 2; 
	e = msgbuf + C_MAX_MSGSIZE;

	/* Do search */

	retcnt = -1;
	res = 0; 
	tv.tv_sec = SEARCH_TIMEOUT; tv.tv_usec = 0;
	n = ldap_search_st(ld, basedn, LDAP_SCOPE_SUBTREE, filter,
			   map->ldap_atrs, 0, &tv, &res);

	if (n == -1 || !res) { ldap_perror(ld, "radldap: Could not search"); 
			       goto srch_done; }

	/* Loop through entries */

	for(ent = ldap_first_entry(ld, res), retcnt = 0; 
	    ent; 
	    ent = ldap_next_entry(ld, ent), retcnt++) {

	    /* Show dn if we're debugging */
	    if (debug) {
		s = ldap_get_dn(ld, ent);
		fprintf(stderr, "radldap: Got object: %s\n", s);
		ldap_memfree(s);
	    }

	    /* Walk attribute map, get all values for each */
	    for(n = 0; n < map->entries; n++) {

		vals = ldap_get_values_len(ld, ent, map->ldap_atrs[n]);

		if (debug) fprintf(stderr, "         Object's values for %s:\n",
				   map->ldap_atrs[n]);

	         /* If no values, handle dn pseudo attribute, continue */
		if (!vals) {
		    if (strcasecmp(map->ldap_atrs[n], "dn") == 0) {
			bv.bv_val = ldap_get_dn(ld, ent);
			bv.bv_len = strlen(bv.bv_val);
			o += add_val(o, e - o - 5, map, n, &bv);
			ldap_memfree(bv.bv_val);
		    }
		    continue;
		}

		/* Add values; always keep 5 words reserved */
		for(v = vals; *v; v++) o += add_val(o, e - o - 5, map, n, *v);

		/* We're done with this attribute so free value array */
		ldap_value_free_len(vals);

	    }   /* next attribute */

	}   /* next object */

srch_done:
	/* Free search result, if any */
	if (res) ldap_msgfree(res);

	/* Add int attribute with found object count */
	*o++ = htonl(C_DS_INTERNAL); 
	*o++ = htonl(C_VND_ANY); 
	*o++ = htonl(C_DI_INT); 
	*o++ = htonl(4); 
	*o++ = htonl(retcnt);

	/* Write size in msgbuf and return it */
	*len = (o - msgbuf) << 2;
	msgbuf[1] = htonl(*len);

	return retcnt;
}


/*
 * MAIN
 */


int main(int argc, char **argv)
{
	static U_INT32_T msgbuf[(C_MAX_MSGSIZE >> 2) + 1];
	U_INT32_T spc, vnd, atr, len, *i, *e;
	int len_cred[2], len_str[2], res;
	char *cred[2], *str[2];
	ATRMAP *map;
	LDAP *ld;

	/* Parse options, read map, setup LDAP connection */
	parseoptions(argc, argv);
	map = setup_atrmap();
	ld = setup_ldap();

	/* Request loop */
	fprintf(stderr, "radldap: Ready for requests.\n");
	for(;;) {

		/*
		 * Get the request from OpenRADIUS
		 */

		/* Read header */
		if (read(0, msgbuf, 8) != 8) { perror("radldap: read"); break; }
		if (ntohl(msgbuf[0]) != 0xbeefdead) {
			fprintf(stderr, "radldap: Invalid magic 0x%08x!\n", 
				ntohl(msgbuf[0])); 
			break;
		}
		msgbuf[0] = htonl(0xdeadbeef);
		len = ntohl(msgbuf[1]);
		if (len < 8 || len > sizeof(msgbuf) - 4) {
			fprintf(stderr, "radldap: Invalid length %d!\n", len); 
			break;
		}

		/* Read rest of message */
		if (read(0, msgbuf + 2, len - 8) != len - 8) {
			perror("radldap: read"); 
			break;
		}

		/* Default LDAP status if we don't get to searching */
		res = 0;

		/*
		 * Loop through the attributes. We're interested in the last
		 * instance of 'User-Name' (space C_DS_RAD_ATR, vendor 0, attr.
		 * C_DI_USER_NAME), 'User-Password' (C_DI_USER_PASSWORD), and 
		 * the last two instances of 'str' (space C_DS_INTERNAL, 
		 * vendor 0, attr. C_DI_STR).
		 */

		cred[0] = cred[1] = str[0] = str[1] = 0;
		len_cred[0] = len_cred[1] = len_str[0] = len_str[1] = 0;
		e = msgbuf + (len >> 2);
		for(i = msgbuf + 2; i < e; i += ((len + 3) >> 2) + 4) {

			/* Get space/vendor/attribute/length */
			spc = ntohl(i[0]); vnd = ntohl(i[1]);
			atr = ntohl(i[2]); len = ntohl(i[3]);
			if (debug) {
				fprintf(stderr, "radldap: got space %d, vendor "
						"%d, attribute %d, len %d\n",
					spc, vnd, atr, len);
			}

			if (spc == C_DS_RAD_ATR && vnd == C_VND_ANY &&
			    (atr == C_DI_USER_NAME ||
			     atr == C_DI_USER_PASSWORD)) {

				cred[atr == C_DI_USER_PASSWORD] = (char *)(i+4);
				len_cred[atr == C_DI_USER_PASSWORD] = len;
			}
			else if (spc == C_DS_INTERNAL && 
				 vnd == C_VND_ANY &&
				 atr == C_DI_STR) {

				str[0] = str[1]; len_str[0] = len_str[1];
				str[1] = (char *)(i + 4); len_str[1] = len;
			}
		}

		/*
		 * If we run in bind-per-request mode, bind each time, using 
		 * the credentials found in User-Name and User-Password. If
		 * the latter is absent, bind anonymously.
		 */

		if (!binddn) {

			/* Zero-terminate credentials; convert NULL to "" */

			if (cred[0]) cred[0][len_cred[0]] = 0; else cred[0]="";
			if (cred[1]) cred[1][len_cred[1]] = 0; else cred[1]="";

			if (do_bind(ld, cred[0], cred[1])) {
				if (debug) ldap_perror(ld, "radldap: Bind "
							   "failed");
				len = set_nak(msgbuf); 
				goto reply;
			}
		}

		/*
		 * Check and zero terminate the strings. We can only safely do 
		 * so after fully parsing msgbuf as we're likely to clobber 
		 * some space fields.
		 */

		if (!str[0] || !str[1]) {
			fprintf(stderr, "radldap: Missing str attribute(s)!\n");
			len = set_nak(msgbuf); goto reply;
		}
		str[0][len_str[0]] = 0; str[1][len_str[1]] = 0;

		/*  
		 * Perform subtree search under the base dn specified in the 
		 * first 'str' using the filter specified in the second and
		 * translate the returned attributes using our mapping table.
		 * If there's a serious LDAP error, we rebind after the reply.
		 */

		res = do_search(ld, str[0], str[1], map, msgbuf, &len);
		
		/*
		 * Send a reply with size 'len'; unbind/rebind if that was asked
		 */
reply:
		if (write(1, msgbuf, len) != len) { perror("radldap: write"); 
						    break; }
		if (res < 0 || unbind_after_req) {
			signal(SIGPIPE, SIG_IGN);
			ldap_unbind_s(ld);
			signal(SIGPIPE, SIG_DFL);
			ld = setup_ldap();
		}
	}

	/* At least try to unbind before exiting; we may leak memory in buggy 
	   servers otherwise */
	signal(SIGPIPE, SIG_IGN);
	ldap_unbind_s(ld);
	signal(SIGPIPE, SIG_DFL);

	return 1;
}



syntax highlighted by Code2HTML, v. 0.9.1