/*
 *
 * Murat's Ident Daemon
 * 
 * (C) 1998-2002 Murat Deligonul
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  
 *
 * ------------------------
 * FIXME:
 *        * Checks for idle time limits and such are not
 *          very efficient (at all?) right now.
 *
 *        * num_conns and num_unix ought to be handled by the
 *          linkedlist code. That is, the linkedlist should know
 *          how many elements it has when we ask for it.
 *
 *        * is_valid_num works but is borken
 *        
 *        * Fake ident lifetime seems to not work reliably?
 *          They get deleted, but sometimes after the life
 *          period.
 *
 *        * I need to eliminate defs.h and make it all command
 *          line configurable.
 *
 *        * On FreeBSD socket name is truncated??
 *
 */

#include "autoconf.h"

#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#include <stdarg.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <netinet/in.h>
#include <signal.h>
#include <syslog.h>
#include <time.h>
#include <ctype.h>
#include <errno.h>
#include <sys/time.h>

#include "identd.h"
#include "defs.h"

#include "lib_mdidentd.h"

static int daemon(bool cr);

static int unix_listen_fd, listen_fd;
static bool foreground, auto_kill;  /* Some options */
bool no_real;
static uid_t uid;                   /* Become this user after binding sock */
static unsigned int hard_limit;     /* Max # of fake idents */
static unsigned int num_conns;      
static unsigned int num_unix;
static char * identd_path;          /* where secondary identd resides */
static char ** identd_args;
static int num_cmdargs;
static int max_time;               /* How long fake ident requests will live */
    
static list<conn> conns;      /* store the connections on port 113 */
static list<conn> unix_conns; /* store connections to the domain socket */
static ilist * table;               /* store all of the fake idents */

int gettok(const char *, char *, unsigned long, char, int, bool = 0);
int safe_strcpy(char *, const char *, unsigned long );
int fdprintf(int fd, const char *, ...);

const char *MDIDENTD_VERSION = "0.85.5";

inline void usage(void) 
{
    fprintf(stderr, "mdidentd v%s - Murat Deligonul (druglord@freelsd.org) - (C) 1998-2002\n"
                   "\nUsage:\n"
                   "mdidentd [-f|-k|-h|-u] <identd> [options]\nWhere:\n"
                   "-u <userid>: Setuid to this user after binding sockets\n"
                   "-h <number>: Hard fake ident limit (max # of fake idents to store at once\n"
                   "-f:          Stay in foreground\n"
                   "-k:          Immediately kill fake ident requests after they have been used\n"
                   "-r:          Prevent users from using setting idents that are real users on this machine\n"
                   "\n"
                   "identd:      The path to your existing ident daemon.\n"
                   "               It will be launched by mdidentd if needed\n"                           
                   "               This is a required argument. Try /usr/sbin/in.identd if unsure.\n"
                   "options      Optional arguments to pass on to the ident daemon mentioned above\n",
                   MDIDENTD_VERSION);
            
}
     

/*
 * FIXME: '6667ax a' is valid according to this
 *        but "ax66667" is not
 */
static inline bool is_valid_num(const char * num)
{
    bool success = 0;
    do {
        if (!isdigit(*num) && !isspace(*num) && !(*num == '\r' || *num == '\n'))
            return 0;
        success = 1;
    } while (*++num); 
    return 1;
}



/*
 * Parse arguments including:
 *   the identd to spawn, and what arguments to give to it
 *   userid to become after getting port
 *   path of domain socket to create
 *   how long to keep custom idents alive.
 *
 * We don't make copies of the stuff in argv but that should be ok
 * for now.
 */
static bool parse_cmdline(int argc, char ** argv)
{
    int cmdarg_start = 0;
    listen_fd = unix_listen_fd = -1;
    num_cmdargs = 0;
    foreground = auto_kill = 0;
    uid = 0;
    hard_limit = 200;
    identd_args = 0;

    num_unix = num_conns = 0;
    
    if (argc < 2)
    {
        usage();
        return 0;
    }
    for (int y = 1; y < argc; y++)
    {
        switch (argv[y][0])
        {
        case '-':
            switch (argv[y][1])
            {
            case 'r':
                no_real = 1;
                break;
            case 'f':
                foreground = 1;
                break;
            case 'k':
                auto_kill = 1;
                break;
            case 'u':
                char * id;
                id = argv[y+1];
                if (!id)
                {
                    fprintf(stderr, "Option '-u' requires numeric user id to follow it\n");
                    exit(1);
                }
                uid = (uid_t) atoi(id);
                y++;
                break;
            case 'h':
                id = argv[y+1];
                if ((!id) || !(hard_limit = (unsigned) atoi(id))) 
                {
                    fprintf(stderr, "Option '-h' requires a number greater than 0\n");
                    exit(1);
                }
                y++;
                break;
            default:
                fprintf(stderr, "Unknown option %c, but continuing anyway\n", argv[y][1]);
            }
            break;

        default:
            /* 
             * Assume this is the start of the path & cmdline 
             * for the other identd. argv[y] is the executable
             * and the rest is the arguments.
             * We set the other program's argv[0] ourselves.
             */
            identd_path = argv[y];
            cmdarg_start = y + 1;
            num_cmdargs = argc - (cmdarg_start) + 1;
            identd_args = new char*[num_cmdargs + 1 /* need one argument for argv[0] 
                                                     * to be given to spawned identd */ 
                                                + 1 /* and one for NULL */];
            identd_args[0] = identd_path;           /* set argv[0] here */
            for (int q = 1; q - 1 < num_cmdargs; q++)
                identd_args[q] = argv[cmdarg_start + q - 1];
            identd_args[num_cmdargs] = NULL;
         
            /* we're done */
            return 1;
        }
    }                   
    return 1;
}



static int listen(unsigned short port)
{
    struct sockaddr_in sin;
    int parm = 1;

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char * ) &parm, sizeof(int));    
    if (listen_fd < 0
            || bind(listen_fd, (struct sockaddr *) &sin, sizeof(sin)) < 0
            || ::listen(listen_fd, 5) < 0)
    {
        close(listen_fd);
        listen_fd = -1;
        return 0;
    }
   
    return 1;   
}

/*
 * Note: this effectively destroys whatever file 
 * is at 'path'.
 *
 * Return: 1 success
 *         0 failure - check errno
 */
static int listen_unix(char const * path)
{
    struct sockaddr_un thing;
    size_t len;
    
    if ((unix_listen_fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
        return 0;
      
    unlink(path);
    memset(&thing, 0, sizeof(thing));
    
    thing.sun_family = AF_UNIX;
    safe_strcpy(thing.sun_path, path, sizeof(thing.sun_path));
    len = sizeof(thing.sun_family) + strlen(thing.sun_path);
    
    if (bind(unix_listen_fd, (struct sockaddr *) &thing, len) < 0
        || ::listen(unix_listen_fd, 3) < 0)
    {
        unix_listen_fd = -1;
        return 0;
    }
    /* Must make it world writable if we expect to receive connections 
       on it */
    fchmod(unix_listen_fd, 0777);
    return 1;
}

static int service_unix(conn * c)
{
    table->read_from(c->fd);
    close_conn(c);
    return -1;
}


/*
 * Accept domain socket connection.
 * Does limits and such. 
 */
static int accept_unix_connection(int fd)
{
    struct sockaddr_un thing;
    socklen_t x = sizeof(thing);
    int fd2 =  accept(fd, (struct sockaddr *) &thing, &x);
    if (fd2 < 0)
        return 0;
    if (table->get_num() >= hard_limit)
    {
        syslog(LOG_ERR, "Not accepting domain socket connection: hard limit on fake idents exceeded");
        close(fd2);
        return 0;
    }
    conn * c = new conn;
    if (!c)
    {
        syslog(LOG_ERR, "accept: memory allocation error after accepting connection on domain sock");
        close(fd2);
        return 0;
    }
    c->fd = fd2;
    c->bytes_in = 0;
    c->time = time(NULL);
    memset(c->input_buff, 0, sizeof(c->input_buff));
    unix_conns.add(c);
    num_unix++;

    return 1;
}

static int accept_connection(int fd)
{
    struct sockaddr_in sin;
    socklen_t len = sizeof(sin);
    int fd2 = accept(fd, (struct sockaddr *) &sin, &len);
    if (fd2 < 0)
    {
        syslog(LOG_ERR, "accept: %s", strerror(errno));
        return 0;
    }
    
    conn * c = new conn;
    if (!c)
    {
        syslog(LOG_ERR, "accept: memory allocation error after accepting");
        close(fd2);
        return 0;
    }
    c->fd = fd2;
    c->bytes_in = 0;
    c->time = time(NULL);
    memset(c->input_buff, 0, sizeof(c->input_buff));
    conns.add(c);
    num_conns++;
    return 1;
}

/*
 * We need to read with MSG_PEEK, because the data must also be readable
 * to the other ident daemon if it needs to be spawned.
 * 
 * Problems:
 * Socket will still be marked as readable afterwards. Select() will indicate
 * so. Will cause infinite loop. For that reason if there is no valid
 * ident response that we can reply to in the first recv attempt,
 * then the request will be given to the other identd. Better fix for this 
 * issue? I don't know!
 *
 * Return -1 if 'c' should be destroyed. 
 */
    
static int service(conn * c)
{
    c->bytes_in = recv(c->fd, c->input_buff, sizeof(c->input_buff) - 1, MSG_PEEK);
    if (c->bytes_in <= 0)
    {
        if (errno)
            syslog(LOG_INFO, "Connection closed: %s", strerror(errno));
        close_conn(c);
        return -1;
    } 
   
    /* We must add null char to the end */
    c->input_buff[c->bytes_in] = 0;

    reply(c);
    return -1;
}

static void close_conn(conn * c)
{
    close(c->fd);
    c->fd = -1;
    c->bytes_in = 0;
}

/*
 * Read the ident request from its input buffer and 
 * see if matches any of our fake ones, if not..
 * its time to load the user's other identd.
 * 
 * Currently replies system name as 'OTHER'.
 */
static int reply(conn * c)
{
    char port1[6], port2[16];
    unsigned short p1 = 0, p2 = 0;
    
    /* 
     * We must have \n or \r or we drop it 
     */
    if (strchr(c->input_buff, '\r') || strchr(c->input_buff, '\n'))
    {
        if ((gettok(c->input_buff, port1, sizeof port1, ',', 1))
             && (gettok(c->input_buff, port2, sizeof port2, ',', 2))
             && (p1 = atoi(port1))
             && (p2 = atoi(port2))
             && is_valid_num(port1)
             && is_valid_num(port2))
        {
            char ident[50];
            struct sockaddr_in sin;
            socklen_t len = sizeof(sin);
            getpeername(c->fd, (struct sockaddr *) &sin, &len);
            if (table->lookup(p1, p2, ident, sizeof(ident)))
            {
                syslog(LOG_INFO, "Request from %s for: %d , %d ", inet_ntoa(sin.sin_addr), p1, p2);
                syslog(LOG_INFO, "(fake) reply: %d, %d : USERID : OTHER :%s", p1, p2, ident);
                fdprintf(c->fd, "%d , %d : USERID : OTHER :%s\r\n",
                        p1, p2, ident);
                if (auto_kill)
                    table->remove(p1, p2, ident);
                close_conn(c);
                return 1;
            }
        }
    }
    /* Have sex */
    switch (fork())
    {
    case 0:
         /* 
          * This is the child: close out unneeded fds, 
          * redirect stdin/err/out to socket.
          */
        close(unix_listen_fd);
        close(listen_fd);
        dup2(c->fd, 0);
        /* close_conn(c); */
        delete c;
        dup2(0, 1);
        dup2(0,2);
        signal(SIGPIPE, SIG_DFL);
        signal(SIGTERM, SIG_DFL);
        signal(SIGINT, SIG_DFL);
        signal(SIGALRM, SIG_DFL);
        if (execv(identd_path, identd_args) < 0)
            syslog(LOG_ERR, "Unable to spawn %s: %s", identd_path, strerror(errno));
        /* unreached */        
        exit(0);
        return 1;
    
    case -1:
        syslog(LOG_ERR, "Can't spawn %s: fork: %s", identd_path, strerror(errno));
    default:
        close_conn(c); 
        break;
    }
    return 1;
}

/*
 * Start the server and enter the main loop.
 * Create fd_sets, select, poll ilist.
 * Blah blah.
 */
static int start_server(time_t max)
{
    fd_set fds;
    struct timeval tv;
    time_t select_time = time(NULL);
    time_t curtime;
    conn * c;
    max_time = max;

    table = new ilist(max_time);
    if (!table)
        return fprintf(stderr,"Memory allocation failure. Bailing out.\n"), 1;
    if (!listen(113))
        return perror("While binding to port 113"), 1;
    if (!listen_unix(MDIDENTD_SOCK_PATH))
        return perror(MDIDENTD_SOCK_PATH), 1;

    /* we already did this. but it doesn't work. i dunno why.
     * i am messing up somwhere. dunno where. but we will do it again.
     * and it will work. so there!! */
    chmod(MDIDENTD_SOCK_PATH, 0777);
#ifndef NOFORK
    if (!foreground)    
    {
        switch (daemon(1))
        {
        case -1:
            perror("While trying to go into the background");
            return 0;
        case 0:
            break;
        default:
            return 0;
        }
    }
#endif
    if (setuid(uid) != 0)
        syslog(LOG_ERR, "setuid(%d) failed: %s (but continuing anyway)", uid, strerror(errno));
    
    /* Set up signals */
    struct sigaction sa;
    sa.sa_flags = 0;
    sa.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &sa, NULL);
    sa.sa_handler = &sighandler;
    sigaction(SIGCHLD, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGHUP, &sa, NULL);
    sigaction(SIGUSR1, &sa, NULL);
    
    /* syslog setup */
    openlog("mdidentd", LOG_PID, LOG_DAEMON);
    syslog(LOG_INFO, "Version %s starting. Using %s as secondary ident daemon.", MDIDENTD_VERSION, identd_path);
    
    list_iterator<conn> i(&conns);
    list_iterator<conn> i2(&unix_conns);

    while (69)
    {
        int hf = listen_fd > unix_listen_fd ? listen_fd : unix_listen_fd;
        i.set(0);
        i2.set(0);
        FD_ZERO(&fds);
        FD_SET(listen_fd, &fds);
        FD_SET(unix_listen_fd, &fds);

       	while (i.has_next())
       	{
			c = i.next();
            FD_SET(c->fd, &fds);
            hf = (hf > c->fd) ? hf : c->fd;
        }

        while (i2.has_next())
        {
        	c = i2.next();
            FD_SET(c->fd, &fds);
            hf = (hf > c->fd) ? hf : c->fd;
        }
        
        tv.tv_sec  = max_time;
        tv.tv_usec = 0;
        /* 
         * Update the select time counter only if the new time would
         * be more than 'max_time' newer than the old one.
         */
        if ((select_time - time(&curtime)) < -max_time)
            select_time = curtime;

        switch (select(hf + 1, &fds, (fd_set *) NULL, (fd_set *) NULL, max_time ? &tv : (struct timeval *) NULL))
        {
        case 0:
            /* This is starting to look like the linux kernel... */
            goto check_time;    
        case -1:
            if (errno != EINTR)
            {
                syslog(LOG_ERR, "select: %s -- bailing out", strerror(errno));
                return 0;
            }
            continue;
        }

        
        if (FD_ISSET(unix_listen_fd, &fds))
            accept_unix_connection(unix_listen_fd);
        if (FD_ISSET(listen_fd,&fds))
            accept_connection(listen_fd);
        
        /*
         * FIXME: these ought to be combined in a singe loop 
         */
        i.set(0);
        i2.set(0);
        time(&curtime);
        while (i.has_next())
        {
        	c = i.next();
            if (c->fd != listen_fd && FD_ISSET(c->fd, &fds) && service(c) == -1)
            {
                delete c;
                i.remove();
                num_conns--;
            } /* Check for idlers while we're here */
            else if (c->fd != listen_fd && curtime - c->time >= MAX_IDENT_WAIT_TIME)
            {
                struct sockaddr_in sin;
                socklen_t size = sizeof(sin);
                getpeername(c->fd, (struct sockaddr *) &sin, &size);
                syslog(LOG_INFO, "Killing idling connection from %s", inet_ntoa(sin.sin_addr));
                close_conn(c);
                delete c;
                i.remove();
                num_conns--;
            }
        }

        while (i2.has_next())
        {
        	c = i2.next();
            if (c->fd != unix_listen_fd && FD_ISSET(c->fd, &fds) &&  service_unix(c) == -1)
            {
                delete c;
                i2.remove();
                num_unix--;
            } /* Check for idlers while we're here */
            else if (c->fd != unix_listen_fd && curtime - c->time >= MAX_UNIX_WAIT_TIME)
            {
                close_conn(c);
                syslog(LOG_INFO, "Killing idling connection on domain socket");
                delete c;
                i2.remove();
                num_unix--;
            }
        }

check_time:     
        /*
         * Check for expired fake request and other things 
         */
        if ((curtime - select_time) >= max_time)
            table->kill_expired(curtime);
    }   
    return 1;
}

int main(int argc, char **argv)
{
    if (parse_cmdline(argc, argv))
        return start_server(FAKE_IDENT_LIFETIME);
    return 1;
}

void sighandler(int sig)
{
    switch (sig)
    {
    case SIGCHLD:
        /* Collect exit status from exiting child */
        wait(&sig);
        break;

    case SIGTERM:
    case SIGINT:
        syslog(LOG_INFO, "Exiting on signal %d", sig);
        closelog();
        delete[] identd_args;
        identd_args = 0;
        exit(0);   

    case SIGHUP:
        syslog(LOG_INFO, "HANGUP signal, don't know what to do.");
        break;
        
    case SIGUSR1:
        syslog(LOG_INFO, "Dumping stats");
        syslog(LOG_INFO, "Active unix conns: %d  Inet conns: %d", num_unix, num_conns);
        syslog(LOG_INFO, "Fake ident requests in memory: %lu; lifetime is %d", table->get_num(), 
               FAKE_IDENT_LIFETIME);
        syslog(LOG_INFO, "   hard limit is %d", hard_limit);
        syslog(LOG_INFO, "Number served: billions and billions");
        break;

            
    }
}

/*
 * Return pid of child to parent.
 * 0 to child.
 * -1 on error.
 */
static int daemon(bool cr)
{
    pid_t xx;
    switch ((xx = fork()))
    {
    case 0:
        /* Child */
        setsid();
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
        if (cr)
            chroot("/");
        return 0;

    case -1:
        return -1;

    default:
        return xx;

    }
}


/*
 *  Gets which'th token, seperated by sep
 *  return 1 on success 0 on failure.
 *  Last argument indicates whether.
 *
 *  Null argument can be passed as buffer to indicate you only
 *  want to check the existance of a token.
 */
int gettok(const char *p, char *buffer, unsigned long maxsize,
           char sep, int which, bool get_rest)
{
    int numfound = 1;
    /* remove leading seps */
    while (*p == sep)
        p++;
    do {
        if (numfound == which && *p)
        {
            char * next = strchr(p, sep);
            if ((get_rest && buffer) || (!next /* && *(p+1) */) )
            {
                if (buffer)
                    safe_strcpy(buffer, p, maxsize);
                return 1;
            }
            else if (next)
            {
                if (buffer)
                {
                    u_long bytes2copy = (next - p) + 1; /* how many bytes must be copied, including
                                            NULL character */
                    if (bytes2copy >= maxsize)
                        bytes2copy = maxsize;
                    strncpy(buffer, p, bytes2copy);
                    buffer[bytes2copy - 1] = 0;
                }
                return 1;
            }
            return 0;
        }
        /* check if this letter == sep, and the next letter is not sep */
        if (*p == sep && *(p+1) != sep)
            numfound++;
    } while (*++p);
    /* failed */
    return 0;
}


/*
 *  Copies src to dest, pretends dest is maxsize long.
 *  will truncate by 1 if needs room to add \0 (bad?)
 */
int safe_strcpy(char *dest, const char *src, unsigned long maxsize)
{
    unsigned long len = strlen(src);

    if (len >=maxsize)
        len = maxsize - 1;
    memcpy(dest, src, len);
    dest[len] = 0;
    return 1;
}

int fdprintf(int fd, const char *format, ...)
{
    static char __mbuffer[1024];
    __mbuffer[0] = 0;
    va_list ap;
    va_start(ap, format);
    int len = vsnprintf(__mbuffer, sizeof(__mbuffer), format, ap);
    va_end(ap);
    return (write(fd, __mbuffer, len));
}


syntax highlighted by Code2HTML, v. 0.9.1