/*
 * $Id: maxsess.c,v 1.9 2006/12/13 01:11:37 heas Exp $
 *
 * Copyright (c) 1995-1998 by Cisco systems, Inc.
 *
 * Permission to use, copy, modify, and distribute this software for
 * any purpose and without fee is hereby granted, provided that this
 * copyright and permission notice appear on all copies of the
 * software and supporting documentation, the name of Cisco Systems,
 * Inc. not be used in advertising or publicity pertaining to
 * distribution of the program without specific prior permission, and
 * notice be given in supporting documentation that modification,
 * copying and distribution is by permission of Cisco Systems, Inc.
 *
 * Cisco Systems, Inc. makes no representations about the suitability
 * of this software for any purpose.  THIS SOFTWARE IS PROVIDED ``AS
 * IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
 * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "tac_plus.h"

#ifdef MAXSESS

#if HAVE_CTYPE_H
# include <ctype.h>
#endif

char *wholog = WHOLOG_DEFAULT;

/*
 * initialize wholog file for tracking of user logins/logouts from
 * accounting records.
 */
void
maxsess_loginit(void)
{
    int fd;

    fd = open(wholog, O_CREAT | O_RDWR, 0600);
    if (fd < 0) {
	report(LOG_ERR, "Can't create: %s", wholog);
    } else {
	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "Initialize %s", wholog);
	}
	close(fd);
    }
}

/*
 * Given a port description, return it in a canonical format.
 *
 * This piece of goo is to cover the fact that an async line in EXEC
 * mode is known as "ttyXX", but the same line doing PPP or SLIP is
 * known as "AsyncXX".
 */
static char *
portname(char *oldport)
{
    char *p = oldport;

    if (!strncmp(p, "Async", 5) || !strncmp(p, "tty", 3)) {
	while (!isdigit((int) *p) && *p) {
	    ++p;
	}
    }
    if (!*p) {
	if (debug & DEBUG_ACCT_FLAG)
	    report(LOG_DEBUG, "Maxsess -- Malformed portname: %s", oldport);
	return(oldport);
    }
    return(p);
}

/*
 * Seek to offset and write a buffer into the file pointed to by fp
 */
static void
write_record(char *name, FILE *fp, void *buf, int size, long offset)
{
    if (fseek(fp, offset, SEEK_SET) < 0) {
	report(LOG_ERR, "%s fd=%d Cannot seek to %d %s",
	       name, fileno(fp), offset, strerror(errno));
    }
    if (fwrite(buf, size, 1, fp) != 1) {
	report(LOG_ERR, "%s fd=%d Cannot write %d bytes",
	       name, fileno(fp), size);
    }
}

static void
process_stop_record(struct identity *idp)
{
    int recnum;
    struct peruser pu;
    FILE *fp;
    char *nasport = portname(idp->NAS_port);

    /* If we can't access the file, skip all checks. */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	report(LOG_ERR, "Can't open %s for updating", wholog);
	return;
    }
    tac_lockfd(wholog, fileno(fp));

    for (recnum = 0; 1; recnum++) {

	fseek(fp, recnum * sizeof(struct peruser), SEEK_SET);

	if (fread(&pu, sizeof(pu), 1, fp) <= 0) {
	    break;
	}

	/* A match for this record? */
	if (!(STREQ(pu.NAS_name, idp->NAS_name) &&
	    STREQ(pu.NAS_port, nasport))) {
	    continue;
	}

	/* A match. Zero out this record */
	bzero(&pu, sizeof(pu));

	write_record(wholog, fp, &pu, sizeof(pu),
		     recnum * sizeof(struct peruser));

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "STOP record -- clear %s entry %d for %s/%s",
		   wholog, recnum, idp->username, nasport);
	}
    }
    fclose(fp);
}

static void
process_start_record(struct identity *idp)
{
    int recnum;
    int foundrec = -1;
    int freerec = -1;
    char *nasport = portname(idp->NAS_port);
    struct peruser pu;
    FILE *fp;

    /* If we can't access the file, skip all checks. */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	report(LOG_ERR, "Can't open %s for updating", wholog);
	return;
    }
    tac_lockfd(wholog, fileno(fp));

    for (recnum = 0; (fread(&pu, sizeof(pu), 1, fp) > 0); recnum++) {
	/* Match for this NAS/Port record? */
	if (STREQ(pu.NAS_name, idp->NAS_name) && STREQ(pu.NAS_port, nasport)) {
	    foundrec = recnum;
	    break;
	}
	/* Found a free slot on the way */
	if (pu.username[0] == '\0') {
	    freerec = recnum;
	}
    }

    /* This is a START record, so write a new record or update the existing
     * one.  Note that we bzero(), so the strncpy()'s will truncate long
     * names and always leave a null-terminated string.
     */

    bzero(&pu, sizeof(pu));
    strncpy(pu.username, idp->username, sizeof(pu.username) - 1);
    strncpy(pu.NAS_name, idp->NAS_name, sizeof(pu.NAS_name) - 1);
    strncpy(pu.NAS_port, nasport, sizeof(pu.NAS_port) - 1);
    strncpy(pu.NAC_address, idp->NAC_address, sizeof(pu.NAC_address) - 1);

    /* Already in DB? */
    if (foundrec >= 0) {

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG,
		   "START record -- overwrite existing %s entry %d for %s "
		   "%s/%s", wholog, foundrec, pu.NAS_name, pu.username,
		   pu.NAS_port);
	}
	write_record(wholog, fp, &pu, sizeof(pu),
		     foundrec * sizeof(struct peruser));
	fclose(fp);
	return;
    }

    /* Not found in DB, but we have a free slot */
    if (freerec >= 0) {

	write_record(wholog, fp, &pu, sizeof(pu),
		     freerec * sizeof(struct peruser));

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added",
		   wholog, freerec, pu.NAS_name, pu.username, pu.NAS_port);
	}
	fclose(fp);
	return;
    }

    /* No free slot. Add record at the end */
    write_record(wholog, fp, &pu, sizeof(pu),
		 recnum * sizeof(struct peruser));

    if (debug & DEBUG_MAXSESS_FLAG) {
	report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added",
	       wholog, recnum, pu.NAS_name, pu.username, pu.NAS_port);
    }
    fclose(fp);
}

/*
 * Given a start or a stop accounting record, update the file of
 * records which tracks who's logged on and where.
 */
void
loguser(struct acct_rec *rec)
{
    struct identity *idp;
    int i;

    /* We're only interested in start/stop records */
    if ((rec->acct_type != ACCT_TYPE_START) &&
	(rec->acct_type != ACCT_TYPE_STOP)) {
	return;
    }
    /* ignore command accounting records */
    for (i = 0; i < rec->num_args; i++) {
	char *avpair = rec->args[i];
	if ((strncmp(avpair, "cmd=", 4) == 0) && strlen(avpair) > 4) {
	    return;
	}
    }

    /* Extract and store just the port number, since the port names are
     * different depending on whether this is an async interface or an exec
     * line. */
    idp = rec->identity;

    switch (rec->acct_type) {
    case ACCT_TYPE_START:
	process_start_record(idp);
	return;

    case ACCT_TYPE_STOP:
	process_stop_record(idp);
	return;
    }
}

/*
 * Read up to n bytes from descriptor fd into array ptr with timeout t
 * seconds.
 *
 * Return -1 on error, eof or timeout. Otherwise return number of bytes read.
 */
int
timed_read(int fd, u_char *ptr, int nbytes, int timeout)
{
    int nread;
    fd_set readfds, exceptfds;
    struct timeval tout;

    tout.tv_sec = timeout;
    tout.tv_usec = 0;

    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    FD_ZERO(&exceptfds);
    FD_SET(fd, &exceptfds);

    while (1) {
	int status = select(fd + 1, &readfds, (fd_set *) NULL,
			    &exceptfds, &tout);

	if (status == 0) {
	    report(LOG_DEBUG, "%s: timeout reading fd %d", session.peer, fd);
	    return(-1);
	}
	if (status < 0) {
	    if (errno == EINTR)
		continue;
	    report(LOG_DEBUG, "%s: error in select %s fd %d",
		   session.peer, strerror(errno), fd);
	    return(-1);
	}
	if (FD_ISSET(fd, &exceptfds)) {
	    report(LOG_DEBUG, "%s: exception on fd %d",
		   session.peer, fd);
	    return(-1);
	}
	if (!FD_ISSET(fd, &readfds)) {
	    report(LOG_DEBUG, "%s: spurious return from select",
		   session.peer);
	    continue;
	}
	nread = read(fd, ptr, nbytes);

	if (nread < 0) {
	    if (errno == EINTR) {
		continue;
	    }
	    report(LOG_DEBUG, "%s %s: error reading fd %d nread=%d %s",
		 session.peer, session.port, fd, nread, strerror(errno));
	    return(-1);		/* error */
	}
	if (nread == 0) {
	    return(-1);		/* eof */
	}
	return(nread);
    }
    /* NOTREACHED */
}

#ifdef MAXSESS_FINGER
/*
 * Contact a NAS (using finger) to check how many sessions this USER
 * is currently running on it.
 *
 * Note that typically you run this code when you are in the middle of
 * trying to login to a Cisco NAS on a given port. Because you are
 * part way through a login when you do this, you can get inconsistent
 * reports for that particular port about whether the user is
 * currently logged in on it or not, so we ignore output which claims
 * that the user is using that line currently.
 *
 * This is extremely Cisco specific -- finger formats appear to vary wildly.
 * The format we're expecting is:

    Line     User      Host(s)		    Idle Location
   0 con 0	       idle		    never
  18 vty 0   usr0      idle		       30 barley.cisco.com
  19 vty 1   usr0      Virtual Exec		2
  20 vty 2	       idle			0 barley.cisco.com

 * Column zero contains a space or an asterisk character.  The line number
 * starts at column 1 and is 3 digits wide.  User names start at column 13,
 * with a maximum possible width of 10.
 */

static int
ckfinger(char *user, char *nas, struct identity *idp)
{
    struct sockaddr_in sin;
    struct servent *serv;
    int count, s, bufsize;
    char *buf, *p, *pn;
    int incr = 4096, slop = 32;
    u_long inaddr;
    char *curport = portname(idp->NAS_port);
    char *name;

    /* The finger service, aka port 79 */
    serv = getservbyname("finger", "tcp");
    if (serv) {
	sin.sin_port = serv->s_port;
    } else {
	sin.sin_port = 79;
    }

    /* Get IP addr for the NAS */
    inaddr = inet_addr(nas);
    if (inaddr != -1) {
	/* A dotted decimal address */
	bcopy(&inaddr, &sin.sin_addr, sizeof(inaddr));
	sin.sin_family = AF_INET;
    } else {
	struct hostent *host = gethostbyname(nas);

	if (host == NULL) {
	    report(LOG_ERR, "ckfinger: gethostbyname %s failure: %s",
		   nas, strerror(errno));
	    return(0);
	}
	bcopy(host->h_addr, &sin.sin_addr, host->h_length);
	sin.sin_family = host->h_addrtype;
    }

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
	report(LOG_ERR, "ckfinger: socket: %s", strerror(errno));
	return(0);
    }
    if (connect(s, (struct sockaddr *) & sin, sizeof(sin)) < 0) {
	report(LOG_ERR, "ckfinger: connect failure %s", strerror(errno));
	close(s);
	return(0);
    }
    /* Read in the finger output into a single flat buffer */
    buf = NULL;
    bufsize = 0;
    for (;;) {
	int x;

	buf = tac_realloc(buf, bufsize + incr + slop);
	x = timed_read(s, buf + bufsize, incr, 10);
	if (x <= 0) {
	    break;
	}
	bufsize += x;
    }

    /* Done talking here */
    close(s);
    buf[bufsize] = '\0';

    if (bufsize <= 0) {
	report(LOG_ERR, "ckfinger: finger failure");
	free(buf);
	return(0);
    }
    /* skip first line in buffer */
    p = strchr(buf, '\n');
    if (p) {
	p++;
    }
    p = strchr(p, '\n');
    if (p) {
	p++;
    }
    /* Tally each time this user appears */
    for (count = 0; p && *p; p = pn) {
	int i, len, nmlen;
	char nmbuf[11];

	/* Find next line */
	pn = strchr(p, '\n');
	if (pn) {
	    ++pn;
	}
	/* Calculate line length */
	if (pn) {
	    len = pn - p;
	} else {
	    len = strlen(p);
	}

	/* Line too short -> ignore */
	if (len < 14) {
	    continue;
	}
	/* Always ignore the NAS/port we're currently trying to login on. */
	if (isdigit((int) *curport)) {
	    int thisport;

	    if (sscanf(p + 1, " %d", &thisport) == 1) {
		if ((atoi(curport) == thisport) &&
		    !strcmp(idp->NAS_name, nas)) {

		    if (debug & DEBUG_MAXSESS_FLAG) {
			report(LOG_DEBUG, "%s session on %s/%s discounted",
			       user, idp->NAS_name, idp->NAS_port);
		    }
		    continue;
		}
	    }
	}
	/* Extract username, up to 10 chars wide, starting at char 13 */
	nmlen = 0;
#if (TAC_IOS_VERSION == 11)
	name = p + 13;
#else
	name = p + 15;
#endif
	for (i = 0; *name && !isspace((int) *name) && (i < 10); i++) {
	    nmbuf[nmlen++] = *name++;
	}
	nmbuf[nmlen++] = '\0';

	/* If name matches, up the count */
	if (STREQ(user, nmbuf)) {
	    count++;

	    if (debug & DEBUG_MAXSESS_FLAG) {
		char c = *pn;

		*pn = '\0';
		report(LOG_DEBUG, "%s matches: %s", user, p);
		*pn = c;
	    }
	}
    }
    free(buf);
    return(count);
}

/*
 * Verify how many sessions a user has according to the wholog file.
 * Use finger to contact each NAS that wholog says has this user
 * logged on.
 */
static int
countusers_by_finger(struct identity *idp)
{
    FILE *fp;
    struct peruser pu;
    int x, naddr, nsess, n;
    char **addrs, *uname;

    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	return(0);
    }
    uname = idp->username;

    /* Count sessions */
    tac_lockfd(wholog, fileno(fp));
    nsess = 0;
    naddr = 0;
    addrs = NULL;

    while (fread(&pu, sizeof(pu), 1, fp) > 0) {
	int dup;

	/* Ignore records for everyone except this user */
	if (strcmp(pu.username, uname)) {
	    continue;
	}
	/* Only check a given NAS once */
	for (dup = 0, x = 0; x < naddr; ++x) {
	    if (STREQ(addrs[x], pu.NAS_name)) {
		dup = 1;
		break;
	    }
	}
	if (dup) {
	    continue;
	}
	/* Add this address to our list */
	addrs = (char **) tac_realloc((char *) addrs,
				      (naddr + 1) * sizeof(char *));
	addrs[naddr] = tac_strdup(pu.NAS_name);
	naddr += 1;

	/* Validate via finger */
	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "Running finger on %s for user %s/%s",
		   pu.NAS_name, uname, idp->NAS_port);
	}
	n = ckfinger(uname, pu.NAS_name, idp);

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "finger reports %d active session%s for %s on %s",
		   n, (n == 1 ? "" : "s"), uname, pu.NAS_name);
	}
	nsess += n;
    }

    /* Clean up and return */
    fclose(fp);
    for (x = 0; x < naddr; ++x) {
	free(addrs[x]);
    }
    free(addrs);

    return(nsess);
}
#endif	/* MAXSESS_FINGER */

/*
 * Estimate how many sessions a named user currently owns by looking in
 * the wholog file.
 */
static int
countuser(struct identity *idp)
{
    FILE *fp;
    struct peruser pu;
    int nsess;

    /* Access log */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	return(0);
    }
    /* Count sessions. Skip any session associated with the current port. */
    tac_lockfd(wholog, fileno(fp));
    nsess = 0;
    while (fread(&pu, sizeof(pu), 1, fp) > 0) {

	/* Current user */
	if (strcmp(pu.username, idp->username)) {
	    continue;
	}
	/* skip current port on current NAS */
	if (STREQ(portname(pu.NAS_port), portname(idp->NAS_port)) &&
	    STREQ(pu.NAS_name, idp->NAS_name)) {
	    continue;
	}
	nsess += 1;
    }

    /* Clean up and return */
    fclose(fp);
    return(nsess);
}

/*
 * is_async()
 * Tell if the named NAS port is an async-like device.
 *
 * Finger reports async users, but not ISDN ones (yay).  So we can do
 * a "slow" double check for async, but not ISDN.
 */
static int
is_async(char *portname)
{
    if (isdigit((int) *portname) || !strncmp(portname, "Async", 5) ||
	!strncmp(portname, "tty", 3)) {
	return(1);
    }
    return(0);
}

/*
 * See if this user can have more sessions.
 */
int
maxsess_check_count(char *user, struct author_data *data)
{
    int sess, maxsess;
    struct identity *id;

    /* No max session configured--don't check */
    id = data->id;

    maxsess = cfg_get_intvalue(user, TAC_IS_USER, S_maxsess, TAC_PLUS_RECURSE);
    if (!maxsess) {
	if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) {
	    report(LOG_DEBUG, "%s may run an unlimited number of sessions",
		   user);
	}
	return(0);
    }
    /* Count sessions for this user by looking in our wholog file */
    sess = countuser(id);

    if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) {
	report(LOG_DEBUG, "user %s is running %d out of a maximum of %d "
	       "sessions", user, sess, maxsess);
    }

#ifdef MAXSESS_FINGER
    if ((sess >= maxsess) && is_async(id->NAS_port)) {
	/*
	 * If we have finger available, double check this count by contacting
	 * the NAS
	 */
	sess = countusers_by_finger(id);
    }
#endif

    /* If it's really too high, don't authorize more services */
    if (sess >= maxsess) {
	char buf[80];

	sprintf(buf,
		"Login failed; too many active sessions (%d maximum)",
		maxsess);

	data->msg = tac_strdup(buf);

	if (debug & (DEBUG_AUTHOR_FLAG | DEBUG_MAXSESS_FLAG)) {
	    report(LOG_DEBUG, data->msg);
	}
	data->status = AUTHOR_STATUS_FAIL;
	data->output_args = NULL;
	data->num_out_args = 0;
	return(1);
    }
    return(0);
}
#endif				/* MAXSESS */


syntax highlighted by Code2HTML, v. 0.9.1