 /*
  * General skeleton for adding options to the access control language. The
  * Makefile describes how this alternative language is enabled. Shell
  * commands will still be available, be it with a slightly different syntax.
  * 
  * The code uses a slightly different format of access control rules. It
  * assumes that an access control rule looks like this:
  * 
  * daemon_list : client_list : option : option ...
  * 
  * An option is of the form "keyword" or "keyword = value". Option fields are
  * processed from left to right. Blanks around keywords, "="  and values are
  * optional. Blanks within values are left alone.
  * 
  * Diagnostics are reported through syslog(3).
  * 
  * Examples of options that are already implemented by the current skeleton:
  * 
  * user = nobody
  * 
  * Causes the process to switch its user id to that of "nobody". This normally
  * requires root privilege.
  * 
  * group = tty
  * 
  * Causes the process to change its group id to that of the "tty" group. In
  * order to switch both user and group ids you should normally switch the
  * group id before switching the user id.
  * 
  * setenv = name value
  * 
  * places a name,value pair into the environment. The value is subjected to
  * %<character> expansions.
  * 
  * spawn = (/usr/ucb/finger -l @%h | /usr/ucb/mail root) &
  * 
  * Executes (in a background child process) the shell command "finger -l @%h |
  * mail root" after doing the %<character> expansions described in the
  * hosts_access(5) manual page. The command is executed with stdin, stdout
  * and stderr connected to the null device. Because options are processed in
  * order, multiple spawn comands can be specified within the same access
  * control rule, though "spawn = command1; command2" would be more
  * efficient.
  * 
  * in.ftpd : ... : twist = /bin/echo 421 Some customized bounce message
  * 
  * Sends some custmized bounce message to the remote client instead of running
  * the real ftp daemon. The command is subjected to %<character> expansion
  * before execution by /bin/sh. Stdin, stdout and stderr are connected to the
  * remote client process. The twist'ed command overlays the current process;
  * it makes no sense to specify other options on the same line after a
  * "twist". The "twist" option was inspired by Dan Bernstein's shuctl daemon
  * wrapper control language.
  * 
  * umask = value
  * 
  * Sets the process file creation mask. Value must be an octal number.
  * 
  * If you compile with -DRFC_OPTION, code is enabled for the following option
  * that does selective rfc931 lookups.
  * 
  * rfc931
  * 
  * Causes the daemon front ends to look up the remote user name with the RFC
  * 931 protocol.
  * 
  * Warnings:
  * 
  * This module uses the non-reentrant strtok() library routine. The options
  * argument to process_options() is destroyed.
  * 
  * There cannot be a ":" character in keywords or values. Backslash sequences
  * are not yet recognized.
  * 
  * In case of UDP connections, do not "twist" commands that use the standard
  * I/O or read(2)/write(2) routines to communicate with the client process;
  * UDP requires other communications primitives.
  * 
  * In case of errors, use clean_exit() instead of directly calling exit(), or
  * your inetd may loop on an UDP request.
  */

/* System libraries. */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <syslog.h>
#include <pwd.h>
#include <grp.h>
#include <ctype.h>

extern char *strtok();
extern char *strchr();
extern void closelog();

/* Local stuff. */

#include "log_tcp.h"

/* List of functions that implement the options. Add yours here. */

static void user_option();		/* execute "user=name" option */
static void group_option();		/* execute "group=name" option */
static void umask_option();		/* execute "umask=mask" option */
static void twist_option();		/* execute "twist=command" option */
#ifdef RFC931_OPTION
static void rfc931_option();		/* execute "rfc931" option */
#endif
static void setenv_option();		/* execute "setenv=name value" */

static char *chop_string();		/* strip leading and trailing blanks */

/* Structure of the options table. */

struct option {
    char   *name;			/* keyword name, case does not matter */
    int     need_value;			/* value required or not */
    void    (*func) ();			/* function that does the real work */
};

/* List of known keywords. Add yours here. */

static struct option option_table[] = {
    "user", 1, user_option,		/* switch user id */
    "group", 1, group_option,		/* switch group id */
    "umask", 1, umask_option,		/* change umask */
    "spawn", 1, shell_cmd,		/* spawn shell command */
    "twist", 1, twist_option,		/* replace current process */
#ifdef RFC931_OPTION
    "rfc931", 0, rfc931_option,		/* do RFC 931 lookup */
#endif
    "setenv", 1, setenv_option,		/* update environment */
    0,
};

static char whitespace[] = " \t\r\n";

/* process_options - process optional access control information */

process_options(options, daemon, client)
char   *options;
char   *daemon;
struct from_host *client;
{
    char   *key;
    char   *value;
    struct option *op;

    /*
     * Light-weight parser. Remember, we may be running as root so we need
     * code that is easy to comprehend.
     */

    for (key = strtok(options, ":"); key; key = strtok((char *) 0, ":")) {
	if (value = strchr(key, '=')) {		/* keyword=value */
	    *value++ = 0;
	    value = chop_string(value);		/* strip blanks around value */
	    if (*value == 0)
		value = 0;			/* no value left */
	}
	key = chop_string(key);			/* strip blanks around key */
	for (op = option_table; op->name; op++)	/* find keyword */
	    if (strcasecmp(op->name, key) == 0)
		break;
	if (op->name == 0) {
	    syslog(LOG_ERR, "bad option or syntax: \"%s\"", key);
	} else if (value == 0 && op->need_value) {
	    syslog(LOG_ERR, "option \"%s\" requires value", key);
	} else if (value && op->need_value == 0) {
	    syslog(LOG_ERR, "option \"%s\" requires no value", key);
	} else {
	    (*(op->func)) (value, daemon, client);
	}
    }
}

/* user_option - switch user id */

/* ARGSUSED */

static void user_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    struct passwd *pwd;
    struct passwd *getpwnam();

    if ((pwd = getpwnam(value)) == 0) {
	syslog(LOG_ERR, "unknown user: \"%s\"", value);
	clean_exit(client);
    } else if (setuid(pwd->pw_uid)) {
	syslog(LOG_ERR, "setuid(%s): %m", value);
	clean_exit(client);
    }
}

/* group_option - switch group id */

/* ARGSUSED */

static void group_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    struct group *grp;
    struct group *getgrnam();

    if ((grp = getgrnam(value)) == 0) {
	syslog(LOG_ERR, "unknown group: \"%s\"", value);
	clean_exit(client);
    } else if (setgid(grp->gr_gid)) {
	syslog(LOG_ERR, "setgid(%s): %m", value);
	clean_exit(client);
    }
}

/* umask_option - set file creation mask */

/* ARGSUSED */

static void umask_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    unsigned mask;
    char    junk;

    if (sscanf(value, "%o%c", &mask, &junk) != 1 || (mask & 0777) != mask) {
	syslog(LOG_ERR, "bad umask: \"%s\"", value);
	clean_exit(client);
    }
    (void) umask(mask);
}

/* twist_option - replace process by shell command */

static void twist_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    char    buf[BUFSIZ];
    int     pid = getpid();
    char   *error;

    percent_x(buf, sizeof(buf), value, daemon, client, pid);

    /* Since we will not be logging in the usual way, do it here and now. */

    syslog(SEVERITY, "twist from %s to %s", hosts_info(client), buf);
    closelog();

    /*
     * Before switching to the shell, set up stdout and stderr in case the
     * Ultrix inetd didn't.
     */

    (void) close(1);
    (void) close(2);
    if (dup(0) != 1 || dup(0) != 2) {
	error = "dup: %m";
    } else {
	(void) execl("/bin/sh", "sh", "-c", buf, (char *) 0);
	error = "/bin/sh: %m";
    }

    /* Can get here only in case of errors. */

#ifdef LOG_MAIL
    (void) openlog(daemon, LOG_PID, FACILITY);
#else
    (void) openlog(daemon, LOG_PID);
#endif
    syslog(LOG_ERR, error);
    clean_exit(client);
}

#ifdef RFC931_OPTION

/* rfc931_option - look up remote user name */

/* ARGSUSED */

static void rfc931_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    if (client->sock_type == FROM_CONNECTED) {
	if (client->sin == 0) {
	    syslog(LOG_ERR, "no socket info for username lookup");
	} else {
	    client->user = rfc931_name(client->sin);
	}
    }
}

#endif

/* setenv_option - set environment variable */

/* ARGSUSED */

static void setenv_option(value, daemon, client)
char   *value;
char   *daemon;
struct from_host *client;
{
    char   *var_name;
    char   *var_value;
    char    buf[BUFSIZ];
    int     pid;

    /*
     * What we get is one string with the name and the value separated by
     * whitespace. Find the end of the name. If that is also the end of the
     * string, the value is empty.
     */

    var_value = value + strcspn(value, whitespace);

    if (*var_value == 0) {			/* just a name, that's all */
	var_name = value;
    } else {					/* expand %stuff in value */
	*var_value++ = 0;
	var_name = chop_string(value);
	pid = getpid();
	percent_x(buf, sizeof(buf), var_value, daemon, client, pid);
	var_value = chop_string(buf);
    }
    if (setenv(var_name, var_value, 1)) {
	syslog(LOG_ERR, "memory allocation failure");
	clean_exit(client);
    }
}

/* chop_string - strip leading and trailing blanks from string */

static char *chop_string(start)
register char *start;
{
    register char *end;

    while (*start && isspace(*start))
	start++;

    for (end = start + strlen(start); end > start && isspace(end[-1]); end--)
	 /* void */ ;
    *end = 0;

    return (start);
}
