/* multifinger.c, 3 February, 1997, D. Glenn Arthur Jr.  */

#include <stdio.h>

#define VERSION "version 0.6T (for Linux at houseoftea.org), 19970203.1615, uses /usr/bin/finger, skips access3"

/*====================================================================*/
/*                                                                    */
/* multifinger.c             Gather info from 'finger' responses from */
/*                                                 multiple machines. */
/*                                                                    */
/* This program fingers a user at access.digex.net.  It displays all  */
/* the usual stuff that 'finger' displays, except that it collects    */
/* the "Last login", "On since", and "Never logged in" lines from all */
/* four access machines (access1.digex.net, access2.digex.net,        */
/* access3.digex.net, and digex4.digex.net).                          */
/*                                                                    */
/* The program has three parts:                                       */
/*                                                                    */
/* 1) a parent which forks and waits for its child to start babbling, */
/*    then decides what parts of the result to display,               */
/* 2) a child which redirects its output to a pipe back to the parent */
/*    then forks itself once for each access machine, and             */
/* 3) a grandchild which execs 'finger' to get the info from one      */
/*    machine.                                                        */
/*                                                                    */
/* Flaws:  This program is slow.  It has to run 'finger' four times   */
/* over the local network.  It also reads and discards three copies   */
/* of the target user's .plan file.  Another problem is that it does  */
/* not detect "connection refused" or timeouts on any of the 'finger' */
/* commands it executes.                                              */
/*                                                                    */
/* Improvements:  Allow multiple arguments on the command line to     */
/* finger more than one user at once.  Allow the nomal options to the */
/* standard 'finger' command to have the same effects here.  Handle   */
/* refused connections, timeouts, and the like.  Move functions that  */
/* ought to be library routines to their own modules.  Set up a table */
/* of which domains/hosts are set up with multiple host machines like */
/* Digex is, so that fingering a user at such a site automagically    */
/* does the right thing.  (Table should be user-configurable.)  WRITE */
/* A 'MAN' PAGE.                                                      */
/*                                                                    */
/* Bugs:  If there is a match on the "real name" field as well as on  */
/* username, remote 'finger' doesn't honor the -m option, so data for */
/* users other than the one specified come back, thus confusing this  */
/* program.  Also, if a user is logged on more than once on one host  */
/* (i.e. using 'screen'), the program is similarly confused.          */
/*                                                                    */
/* Version 0.1  DGA  19950406  Announced on digex.general             */
/* Modified     DGA  19950407  Added comments, internal documentation */
/* Modified     DGA  19950408  Added wordwrap() for help message      */
/* Modified     DGA  19950411  Reorganized, split into functions.     */
/* Modified     DGA  19950417  Changed NUMHOSTS to 5.                 */
/* Version 0.4a DGA  19950711  Temp. "IGNORETHREE" fix.               */
/* Modified     DGA  19951214  Added email address to help message    */
/* Modified     DGA  19951218  Made /usr/ucb/finger explicit.         */
/* Modified     DGA  19961011  Removed "IGNORETHREE".                 */
/* Modified     DGA  19970902  Reinstalled "IGNORETHREE".             */
/* Modified     DGA  19970902  Added some clues for 'ps' to chew on   */
/* Version 0.6T DGA  19980203  Uses /usr/bin/finger for Teahouse vrsn */
/*                                                                    */
/*====================================================================*/


/* Glenn's standard debugging macro */

#define DEBUG    if ( debug ) printf
#define debug    0

/* Subscripts for the argument of pipe() */

#define W_END     1
#define R_END     0

/* The standard files */

#define STDIN     0
#define STDOUT    1
#define STDERR    2


/*====================================================================*/
/* CONFIGURATION CONSTANTS                                            */
/*====================================================================*/

#define BUFFLEN    80
#define NUMHOSTS    5

#define HELP_MESSAGE "\
This program takes one argument, a username, and fingers that user \
at access.digex.net, but unlike the normal 'finger' command it gathers \
the login time ('On since' or 'Last login' times) from _each_ of the \
five access machines.  \
\n********\n\
Notes:  Will not yet correctly handle multiple hits in /etc/passwd \
i.e. situations where the username matches more than one user's \
'realname' field.\
\n\nTemporarily ignoring access3, 'cause it seems to have been dead \
a while and messes things up.\
\n\nEmail bug reports, suggestions, patches, praise to \
glenn@access.digex.net\
\n"
 

/* GLOBAL VARIABLES */

char argzero[80];               /* Keeps argv[0] in a globally        */
                                /* accessible location                */

char childname[6] = "child";

/*====================================================================*/
/*                                                                    */
/* fdgets()                         Like gets() but reads from a pipe */
/*                                                                    */
/* Takes three arguments:  the file descriptor of the "read end" of   */
/* pipe you wanna read, the buff to stuff, and the maximum number of  */
/* bytes to transfer.  Works much like gets() -- copies bytes from    */
/* the pipe to the buffer until it's got N bytes or it hits a newline */
/* or end of file happens.  Returns the number of bytes transferred,  */
/* or -1 to signal end of file (closed pipe).  Ought to work on       */
/* ordinary files as well, but I haven't tried that.                  */
/*                                                                    */
/* Improvements:  This function really ought to read into a buffer    */
/* instead of doing all those single-character read() calls, for the  */
/* sake of efficiency.  Even if the OS buffers read() for us, it's    */
/* still that many more function calls.                               */
/*                                                                    */
/* Created   DGA  19950404                                            */
/* Commented DGA  19950411                                            */
/*                                                                    */
/*====================================================================*/

int    fdgets ( fd , s , length )
int    fd ;                     /* File descriptor from which to read */
char    *s;                     /* Buffer into which to put read data */
int    length;                  /* Maximum number of bytes to read    */

    {
    int     count;              /* Holds # bytes read by call to      */
                                /* read()                             */
    int     sofar;              /* Running count of # bytes read by   */
                                /* this invocation of this function   */
    char    c[2];               /* Temporary buffer to hold single    */
                                /* characters read from fd            */

    /* Read a byte at a time from (fd) until a termination condition  */
    /* (newline, EOF, or max # bytes read) happens.  I guess this     */
    /* really ought to be a single for() statement with no body, just */
    /* to be manly ... Let's see, that'd be:                          */
    /*     "for ( sofar = 0 ; (count = read(fd,c,1) != 0) &&          */
    /*     (c[0] != '\n') && (sofar < length-1) ; s[sofar++]          */
    /*     = c[0] );"                                                 */
    /* but I think I split it up for debugging purposes or something. */

    sofar = 0;
    while ( ( count = read(fd, c, 1) != 0 ) 
         && ( c[0] != '\n' ) && ( sofar < length-1 ) )
        {
        s[sofar++] = c[0];
        }

    /* At this point, (sofar) bytes have been read, so s[sofar-1] is  */
    /* the last byte read.  Stick a null on the end no matter what    */
    /* made us stop.                                                  */

    s[sofar] = '\0';

    /* Figure out why we stopped, and return the EOF indicator (-1)   */
    /* or the number of bytes read, as appropriate.                   */

    if ( count == 0 )               /* Last 1-byte read failed.       */
        return ( -1 );              /* Signal EOF to caller.          */
    else                            /* Note EOF yet.                  */
        return ( sofar );           /* Return # bytes actually read.  */
    }

/*===== end of fdgets() ==============================================*/

/*====================================================================*/
/*                                                                    */
/* wordwrap()                              displays text word-wrapped */
/*                                                                    */
/* Takes a loooong string and wraps it to the specified number of     */
/* columns, breaking at whitespace.  Prepends a prefix to each line   */
/* of output.  I should sit down and write a more versatile set of    */
/* related functions.                                                 */
/*                                                                    */
/* Created  19950408  DGA                                             */
/*                                                                    */
/*====================================================================*/


void wordwrap ( longstring , columns , prefix )
char *longstring;               /* Input text                          */
int  columns;                   /* Number of columns to wrap to        */
char *prefix;                   /* String to prepend to output lines   */

    {
    int left, right, lf;
    char linebuff[256];

    left = 0;

    while ( left + columns < strlen(longstring) )
        {

    /* Start at the right margin and work backwards looking for a space */

        for ( right = left + columns ; longstring[right] != ' ' ; right-- );

    /* Copy what's between our left & right markers to (linebuff)       */

        strncpy ( linebuff , &longstring[left] , right-left );

    /* Check for linefeeds -- we need to handle them specially          */

        for ( lf = 0 ; (lf < columns) && (linebuff[lf] != '\n') ; lf++ );

    /* No line feeds -- terminate (linebuff) and adjust the left marker */

        if ( lf < columns )
            {
            linebuff[lf] = '\0';
            left = left + lf + 1;
            }

    /* Found a line feed -- replace it with a null in (linebuff) and    */
    /* move the left marker to the character after it in the input      */
    /* buffer.  The next pass through the loop will pick up with the    */
    /* character after the linefeed being the start of a complete line. */

        else
            {
            linebuff[right-left] = '\0';
            left = right + 1;
            }

    /* Whatever we've got in (linebuff) now gets printed, with (prefix) */
    /* prepended first.                                                 */

        printf ( "%s%s\n" , prefix , linebuff );
        }
 
    /* When we get out of the while() loop, print out whatever's left */
    /* in the input string.                                           */   

    printf ( "%s%s\n" , prefix , &longstring[left] );
    }

/*===== end of wordwrap() ============================================*/

/*====================================================================*/
/*                                                                    */
/* grandchild()                  Exec's 'finger' after having had its */
/*                                       output redirected to a pipe. */
/*                                                                    */
/* This function is called (NUMHOSTS) times by child().  Before this, */
/* child() has redirected stdout to a pipe back to main().  All this  */
/* function does is use sprintf() to combine its arguments into a     */
/* command string and call execlp() to invoke 'finger' on one host.   */
/*                                                                    */
/* Created  19950411  DGA  Split this off into a separate function.   */
/*                                                                    */
/*====================================================================*/

int grandchild ( user , hostsuffix )
char *user;
int  hostsuffix;

    {
    char argbuff[80];

#ifdef IGNORETHREE
    /* This section forces the program to ignore access3.  As this    */
    /* section is being added, access3 has been unresponsive for a    */
    /* while and I'm not ready to rewrite things so that a dead host  */
    /* doesn't screw things up at the moment.                         */
    /* 11 OCTOBER 1996 -- access3 seems to be back up!                */
    /* 25 MARCH 1997   -- access2 is hosed, so this code is being     */
    /*                    reactivated, modified to refer to access2.  */
    /* 2 SEPTEMBER 1997 - access3 has been dead a while, so ...       */

    if (hostsuffix == 3)
        {
        printf ( "Last login:  (access3 not checked).\n" );
	exit(-1);
        }
#endif

    /* printf ( "This is grandchild number %d.\n" , hostsuffix ); */
    sprintf( argbuff, "%s@access%d.digex.net", user, hostsuffix );
    execlp ( "/usr/bin/finger" , "((mfinger))" , argbuff , NULL );

    /* Note that execlp(), if successful,  _does_not_return_, so this */
    /* point should be an implied exit().  Just in case execlp() does */
    /* return, here's an error message.  Might as well re-use argbuff */
    /* for this, since it's there.                                    */

    sprintf ( argbuff , "%s (child process):  attempt to exec 'finger' failed.\n" , argzero );
    write ( STDERR , argbuff , strlen(argbuff) );
    exit(-1);
    }

/*====================================================================*/
/*                                                                    */
/* child()              Redirect stdout to previously opened pipe and */
/*                    fork() (NUMHOSTS) copies of grandchild() to run */
/*                                             'finger' on each host. */
/*                                                                    */
/* This function redirects its stdout into the pipe opened in main(), */
/* then forks and waits (NUMHOSTS) times to run a copy of             */
/* grandchild() on each host.                                         */
/*                                                                    */
/* Created   19950411  DGA  Split off into separate function.         */
/*                                                                    */
/*====================================================================*/

int child( username , pipe_end , pname )
char *username;
int  pipe_end;
char *pname;

    {
    int  suffix;
    int  grandchild_pid;

	strncpy ( pname , "(mfinger)" , strlen(pname) );

#ifdef DEBUG
	sleep(20);	/* Give time to look up PID for debugger */
        DEBUG ( "This is the child.\n" );
#endif

    /* Redirect stdout so that grandchildren's output gets sent back  */
    /* to main().                                                     */

        if ( dup2 ( pipe_end , STDOUT ) != STDOUT )
            {
            printf ( "%s:  problem redirecting output.\n" , argzero );
            exit(-1);
            }
        
    /* For each host, fork a copy of grandchild() to run 'finger' on  */
    /* it.                                                            */

        for ( suffix = 1 ; suffix <= NUMHOSTS ; suffix++ )
            {
            if ( ( grandchild_pid = fork () ) == 0 )
                grandchild ( username , suffix );

    /* Note that grandchild() _does_not_return_, so this point is an   */
    /* implied exit() if the fork succeeded and this is the grandchild */
    /* fork process.                                                   */

    /* If this is not the grandchild process (fork() had a nonzero     */
    /* return), either fork() failed ...                               */

            else
                {
                if ( grandchild_pid == -1 )
                    {
                    printf ( "%s:  fork() problem.\n" , argzero );
                    exit(-1);
                    }

    /* ... or it succeeded and this is not the grandchild, so we need  */
    /* to wait for the grandchild to terminate before forking another, */
    /* so that the output from the grandchildren isn't interleaved.    */

                else
                    wait ( NULL );
                }
            }

    /* In any case, we've done all that this child of main() should do, */
    /* so let's terminate the process.                                  */

        exit(0);
    }

/*==== End of child() ==================================================*/

/*====================================================================*/
/*                                                                    */
/* main()            'multifinger' -- Finger a user on all four Digex */
/*                         "access" hosts at once, showing last-login */
/*                            times for each host and the rest of the */
/*                                         'finger' output only once. */
/*                                                                    */
/* Apart from checking the command line for -v or -h or the wrong     */
/* number of arguments, what this does is:                            */
/*  1)  Create a pipe.                                                */
/*  2)  Fork -- the child will redirect it's stdout and fork again.   */
/*  3)  Wait for output from child and selectively display it.        */
/* For details on #3, see comments inside final while() loop.         */
/*                                                                    */
/* Created  DGA  19950406                                             */
/* Modified DGA  19950411  Split off child() and grandchild()         */
/*                                                                    */
/*====================================================================*/

int    main ( argc , argv )
int    argc;
char   **argv;

    {
    char    username[10];    /* Holds copy of username to try to finger */
    int     child_pid;        /* Holds return value of fork()          */
    int     state, responses; /* Used to determine what parts of the   */
                              /* output from the grandchild processes  */
                              /* to display, and what to throw away.   */
    char    buffer[81];       /* Holds a line of output from granchild */
    int     apipe[2];         /* Pipe through which grandchildren's    */
                              /* output comes back.                    */

    /* Check the command line.   This section is fairly intuitive (and */
    /* rather brute-force.)                                            */

    if ( argc != 2 )
        {
        printf ( "usage:  %s username\n" , argv[0] );
        printf ( " -or-   %s -h        (for more info)\n" , argv[0] );
        printf ( " -or-   %s -v        (for version)\n" , argv[0] );
        exit(0);
        }

    if ( strncmp ( argv[1] , "-v" , 2 ) == 0 )
        {
        printf ( "%s:  %s\n" , argv[0] , VERSION );
        exit(1);
        }

    if ( strncmp ( argv[1] , "-h" , 2 ) == 0 )
        {
        wordwrap ( HELP_MESSAGE , 67-strlen(argv[0]) , sprintf ( buffer , "%s:    " , argv[0]) );
        exit(1);
        }

    DEBUG ( "DEBUG -- Got past checking command line.\n" );

    /* Set things up -- set globals, copy things where they need to be, */
    /* create the pipe. */

    strncpy ( argzero  , argv[0] , 80 );
    strncpy ( username , argv[1] ,  9 );

    if ( pipe ( apipe ) != 0 )
        {
        printf ( "%s: could not open pipe.\n" , argv[0] );
        exit(-1);
        }

    /* Call fork().  If fork() returns zero, do child(), else continue */
    /* with main().   */

    if ( ( child_pid = fork () ) == 0 )
        child ( username , apipe[W_END] , argv[0] );

    else
        {

    /* fork() returned nonzero, so this is the parent process.  First, */
    /* make sure there really _is_ a child process ...                 */

        if ( child_pid == -1 )
            {
            /* If fork() returned -1, that means it failed. */

            printf ( "%s:  fork() failed.\n" , argv[0] );
            exit(-1);
            }
        
        DEBUG ( "This is the parent.  The child is %d.\n" , child_pid );
        DEBUG ( "debug -- about to hit while() loop in parent.\n" );

    /* Close the parent's copy of the writing end of the pipe, so that */
    /* when the child process terminates (closing its copy of that end */
    /* of the pipe), we'll get an end-of-file.                         */

        close ( apipe[W_END] );

    /* Now all that's left is to read the responses we get through the */
    /* pipe and decide which parts to display.  So what follows is a   */
    /* while() loop to read until the pipe closes, with the body being */
    /* a state machine that handles each of the three phases of the    */
    /* output -- printing everything that comes before the last login  */
    /* time, printing the login times, and printing everything that    */
    /* comes after the last login time.                                */

        state = 1;
        while ( fdgets ( apipe[R_END] , buffer , BUFFLEN ) != -1 )
            {
            switch ( state )
                {

    /* State One -- We're looking at the output of the first 'finger'  */
    /* command (access1).  We've not yet gotten to the Last login / On */
    /* since / Never logged in.  Until we get that, just display what  */
    /* comes back from the grandchildren.  When we _do_ get that line, */
    /* prefix it with the name of the machine (access1), display it,   */
    /* and go to state two.                                            */

                case 1:    /* DEBUG ( "debug:  state=1  " ); */
                    if ((strncmp(buffer,"On since",8)==0)||
                        (strncmp(buffer,"Never lo",8)==0)||
                        (strncmp(buffer,"Last log",8)==0))
                        {
                        state = 2;
                        responses = 1;
                        }
                    if ( state == 1 )
                        puts ( buffer );
                    else
                        printf ( "access%d:  %s\n" , responses , buffer );
                    break;

    /* State Two -- We've already started displaying the login times.  */
    /* We're currently looking at the responses from 'finger' at host  */
    /* two, three, or four, and we're looking for that login time,     */
    /* displaying that and ignoring all else.  Each time we get one,   */
    /* increment (responses), the counter of how many hosts we've      */
    /* gotten our answers from.  Once (responses) gets to NUMHOSTS, we */
    /* know we've gotten all the login times we're going to get, so we */
    /* can go on to state three.                                       */

                case 2:    /* DEBUG ( "debug:  state=2  " ); */
                    if ((strncmp(buffer,"On since",8)==0)||
                        (strncmp(buffer,"Never lo",8)==0)||
                        (strncmp(buffer,"Last log",8)==0))
                        {
                        responses++;
                        printf ( "access%d:  %s\n" , responses, buffer );
                        if ( responses == NUMHOSTS )
                            state = 3;
                        }
                    break;

    /* State Two -- We're looking at the output of the first 'finger'  */
    /* State Three -- We've printed the top section (from the first    */
    /* host), all the login times (from all four hosts), and now we're */
    /* ready to print .project and .plan, so since we're getting the   */
    /* response from the last host, we just display everything we get  */
    /* from here until EOF on the pipe.                                */

                case 3:    /* DEBUG ( "debug:  state=3  "); */
                    puts ( buffer );
                    break;
                }
            }
        DEBUG ( "debug -- past final while() loop in parent.\n" );
        }

    }  /* END OF main() */

