/* userinfo.c -- the default method of printing out the verbose info
   on a given user.  Another choice might be to print out values found
   in a database where each record contained interesting info about
   a user. */

/* Copyright (C) 1988, 1990, 1992  Free Software Foundation, Inc.

   This file is part of GNU Finger.

   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, 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.  */

#include <stdio.h>
#include <config.h>
#include <sys/types.h>
#include <sys/file.h>
#if defined (HAVE_LASTLOG)
#include <lastlog.h>
#else
#ifdef HAVE_UTMPX
#include <utmpx.h>
#else
#include <utmp.h>
#endif
#endif
#include <sys/stat.h>
#include <pwd.h>

#include <packet.h>
#include <general.h>
#include <signal.h>
#include <setjmp.h>
#include <fingerpaths.h>

#ifdef HAVE_VFORK_H
#include <vfork.h>
#endif

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include <errno.h>

#ifndef errno
extern int errno;
#endif

#ifndef SIGCHLD
#define SIGCHLD SIGCLD
#endif

char *path_concat ();

/* Location of the lastlog file.  This is for regular fingering. */
#if defined (HAVE_LASTLOG) && !defined (LASTLOG_FILE)
#define LASTLOG_FILE "/usr/adm/lastlog"
#endif

/* Timeout */
jmp_buf alarmframe;

static void
got_alarm ()
{
  longjmp (alarmframe, !0);
}


/* Child exit */
jmp_buf childframe;

static void
got_chld ()
{
  /* Disable alarm since that frame is no longer active */
  alarm (0);
  signal (SIGALRM, SIG_IGN);
  longjmp (childframe, !0);
}


/* Print HOMEDIR_FILE in home directory of USER on STREAM, prefixed
   with TITLE. Take care to terminate each line CR/LF, and include a
   trailing CR/LF after the last line if it wasn't terminated. Ends the
   whole file with a CR/LF. Returns 0 if an error occured; errno is set
   to error. */

static int
print_file (homedir_file, user, stream, title)
  char *homedir_file, *title;
  struct passwd *user;
  FILE *stream;
{
  char *file = path_concat (user->pw_dir, homedir_file);
  FILE *istream;

  /* This buffer is used only to fragment the input; its
     size poses no limits on line widths. */
  char buff[256];
  
  istream = fopen (file, "r");

  if (istream)
    {
      int last_had_nl;
      
      fprintf (stream, "%s:\r\n", title);
      
      while (istream && fgets (buff, sizeof buff, istream))
	if ((last_had_nl = buff[strlen(buff)-1] == '\n'))
	  {
	    buff[strlen(buff)-1] = 0;
	    fprintf (stream, "%s\r\n", buff);
	  }
	else
	  fprintf (stream, "%s", buff);
      
      /* Make sure we end with CR-LF */
      if (!last_had_nl)
	fprintf (stream, "\r\n");

      fprintf (stream, "\r\n");

      fclose (istream);
      free (file);

      return !0;
    }

  free (file);
  return 0;
}


/* Print lastlog info about USER on STREAM. This is local to the
   current host. */
static int
print_lastlog (user, stream)
  struct passwd *user;
  FILE *stream;
{
#if defined (HAVE_LASTLOG)
  struct lastlog logent;
  int file;
  
  if ((file = open (LASTLOG_FILE, O_RDONLY)) >= 0)
    {
      if ((lseek (file, (long) (user->pw_uid * sizeof (logent)), L_SET) != -1) &&
	  (read (file, &logent, sizeof (logent)) == sizeof (logent)))
	{
	  if (!logent.ll_time)
	    fprintf (stream, "Never logged on.\r\n");
	  else
	    {
	      fprintf (stream, "Last login on %.*s ", sizeof (logent.ll_line),
		       logent.ll_line[0] ? logent.ll_line : "(no tty)");
	      if (logent.ll_host[0])
		fprintf (stream, "from %.*s, ", sizeof (logent.ll_host),
			 logent.ll_host);
	      fprintf (stream, "on %s\r\n", ctime (&(logent.ll_time)));
	    }
	}
      close (file);
    }
#else   /* !HAVE_LASTLOG */
  return 0;
#endif  /* !HAVE_LASTLOG */
}


/* Exec FILE if existent, connecting its input to STREAMP via a pipe
   (set to a new file), as USER, but with the UID of the owner of the
   file. Returns process ID of child if successful, otherwise zero. The
   file must not be writable to anyone except the owner. */

static pid_t
maybe_exec_as_user (file, streamp, user)
  char *file;
  FILE **streamp;
  struct passwd *user;
{
  struct stat statdata;
  pid_t pid = 0;
  int feed[2];


  if (stat (file, &statdata) >= 0)
    {
#ifdef CHECK_OWNER_FINGERRC
      /* Make sure file is owned by user we're execing as. */
      if (statdata.st_uid != user->pw_uid)
	return 0;
#endif

#ifdef CHECK_RDONLY_FINGERRC
      /* Make sure file isn't writable to anyone except owner */
      if (statdata.st_mode & 022)
	return 0;
#endif

      /* Disable timeout in user process. */
      signal (SIGPIPE, SIG_IGN);

      /* Create child */
      pipe (feed);

      if (pid = vfork ())
	{
	  /* Catch SIGCHLD */
	  signal (SIGCHLD, got_chld);

	  /* Redirect stream to child */
	  *streamp = fdopen (feed[1], "w");
	  close (feed[0]);
	}
      else
	{
	  alarm (0);
	  signal (SIGALRM, SIG_IGN);

	  close (0);
	  dup (feed[0]);
	  close (feed[1]);

	  /* Make child stdout and stderr be the TCP stream */
	  if (fileno (*streamp) != 1)
	    {
	      close (1);
	      dup (fileno (*streamp));
	    }

	  if (fileno (*streamp) != 2)
	    {
	      close (2);
	      dup (fileno (*streamp));
	    }

     	  /* Set uid/gid */
	  setuid (user->pw_uid);
	  setgid (user->pw_gid);

	  /* Set default directory */
	  chdir (user->pw_dir);

	  /* Run ~/.fingerrc through user shell */
#ifdef FINGERRC_SHELL
	  execlp (FINGERRC_SHELL, FINGERRC_SHELL, "-c", file, NULL);
#else	  
	  execlp (user->pw_shell, user->pw_shell, "-c", file, NULL);
#endif

	  fprintf (*streamp, "Exec failure - please contact postmaster.\r\n");
	  exit (0);
	}
    }

  return pid;
}


/* Output mail forwarding info for USER on STREAM. */

static int
print_mail_forwarding (user, stream)
  struct passwd *user;
  FILE *stream;
{
  char *forward_file = path_concat (user->pw_dir, ".forward");
  struct stat statdata;
  

  if (!(stat (forward_file, &statdata) < 0 || !statdata.st_size))
    {
      FILE *fw;
      char *rbuf = NULL;
      int rbuf_size = 0;
      
      if (fw = fopen (forward_file, "r"))
	{
	  if (getline (&rbuf, &rbuf_size, fw) > 0)
	    {
	      if (rbuf[strlen(rbuf)-1] == '\n')
		rbuf[strlen(rbuf)-1] = 0;
	  
	      fprintf (stream, "Mail forwarded to %s.\r\n", rbuf);
	      fclose (fw);
	      free (forward_file);
	      free (rbuf);
	      return 1;
	    }

	  if (rbuf)
	    free (rbuf);
	}
    }

  free (forward_file);
  return mail_expand (user->pw_name, stream);
}


/* Display status of mailbox for USER on STREAM. Don't say there's no
   unread mail if MAIL_FORWARDED is non-zero. */

static void
print_mail_status (user, stream, mail_forwarded)
  struct passwd *user;
  FILE *stream;
  int mail_forwarded;
{
  struct stat finfo;
  char *mail_file = path_concat (MAILDIR, user->pw_name);
  
  if (stat (mail_file, &finfo) < 0 || finfo.st_size == 0)
    /* Don't mention there's no mail if mail has been forwarded */
    if (!mail_forwarded)
      fprintf (stream, "No mail.\r\n");
    else ;
  else
    {
      if (finfo.st_atime < finfo.st_mtime)
	{
	  char *idle_time_string (), *temp_string;
	  
	  fprintf (stream, "New mail since %s", ctime (&finfo.st_mtime));
	  temp_string =
	    idle_time_string ((time_t)time ((time_t *)0) - finfo.st_atime);
	  fprintf (stream, "Has not read mail for %s.\r\n", temp_string);
	  free (temp_string);
	}
      else
	{
	  /* Don't mention there's no unread mail if mail has been forwarded */
	  if (!mail_forwarded)
	    fprintf (stream, "No unread mail.\r\n");
	}
    }

  free (mail_file);
}


/* Return packets in HOSTDATA database */
FINGER_PACKET **
get_hostdata ()
{
  FINGER_PACKET **hpackets;
  int f;

  f = open (HOSTDATA, O_RDONLY, 0666);
      
  hpackets = read_packets (f);
      
  if (f != -1)
    {
      close (f);
      sort_packets (hpackets);
    }

  return hpackets;
}


/* Print login info about USER, onto STREAM. Also look in other
   places. Obtains information on the local host if LOCALFLAG is nonzero,
   otherwise looks in the fingerd database for net-global login records.
   Returns number of records listed. */

static int
print_login (user, stream, localflag)
  struct passwd *user;
  FILE *stream;
  int localflag;
{
  int packet_number;
  int packets_output = 0;
  FINGER_PACKET **hpackets;
  extern FINGER_PACKET **get_finger_data ();


  hpackets = localflag ? get_finger_data (1) : get_hostdata ();

  if (hpackets)
    for (packet_number = 0; hpackets[packet_number]; packet_number++)
      if (!xstricmp (user->pw_name, hpackets[packet_number]->name))
	{
	  ascii_packet (hpackets[packet_number], stream, !packets_output);
	  packets_output++;
	}

  
  /* List any old entries in userdata */
  if (!packets_output)
    packets_output = display_last_seen (user->pw_name, stream);
  
  if (packets_output)
    fprintf (stream, "\r\n");
  
  /* Check lastlog if we don't have anything recorded in userdata. */
  
  if (!packets_output)
    if (! print_lastlog (user, stream))
      fprintf (stream, "%s has never logged on.\r\n",
	       pw_real_name (user));

  if (hpackets)
    free_array (hpackets);
}


/* Print out information about user ENTRY on STREAM. Obtain info
   locally if LOCALFLAG is nonzero, otherwise look in the fingerd
   database. */

#if defined (DEBUG)
#define WHAT(X,Y) 0
#else
#define WHAT(TIMEOUT, MSG) \
  (alarm (TIMEOUT), where = (MSG))
#endif

display_long_info (entry, stream, localflag)
  struct passwd *entry;
  FILE *stream;
  int localflag;
{
  int packets_output = 0;
#ifdef SUPPORT_FINGERRC
  char *fingerrc_file = path_concat (entry->pw_dir, ".fingerrc");
#endif
  int mail_forwarded = 0;
  char *where;
  pid_t pid;
  extern int mail_expand ();
  

  WHAT (0, "doing something indeterminable");

  if (setjmp (alarmframe))
    {
      fprintf (stream, "Timeout -- while %s.\r\n", where);
      fflush (stdout);

      return 0;
    }

  signal (SIGALRM, got_alarm);

#ifdef SUPPORT_FINGERRC
  WHAT (10, "doing user-defined processing");
  pid = maybe_exec_as_user (fingerrc_file, &stream, entry);
  free (fingerrc_file);
#endif

  /* Skip all the printing code if the child dies so we don't
     provide output to a pipe without a consumer. */

  if (!setjmp (childframe))
    {
      WHAT (5, "obtaining user login information");

      fprintf (stream, "\n%s (%s)\nHome: %s\nShell: %s\n",
	       pw_real_name (entry),
	       entry->pw_name,
	       entry->pw_dir, entry->pw_shell);      
      
      /* Show mail forwarding, if any */
      WHAT (10, "examining user mail forward file");
      mail_forwarded = print_mail_forwarding (entry, stream);

      /* Display mailbox status */
      WHAT (10, "finding out about unread mail");
      print_mail_status (entry, stream, mail_forwarded);
      
      /* If the user is logged in, show the login packets. */
      WHAT (15, "examining active login database");
      packets_output = print_login (entry, stream, localflag);

      /* Maybe do the .plan file. */
      WHAT (10, "reading user plan");

      if (!print_file (".plan", entry, stream, "Plan"))
	if (errno == EACCES)
	  fprintf (stream, "Plan file exists, but is unreadable.\r\n");
	else
	  fprintf (stream, "No plan.\r\n");
      
      /* Maybe do the .project file. */
      WHAT (10, "reading user project list");

      if (!print_file (".project", entry, stream, "Project")
	  && errno == EACCES)
	fprintf (stream, "Project file exists, but is unreadable.\r\n");

      /* Collect zombie if we're parent. Note: on most USG systems,
	 catching a SIGCHLD will automatically collect zombies. This
	 means we sit here until a signal arrives and we leave this
	 stack frame. On systems where catching a SIGCHLD does not
	 collect zombies, we exit through either - if we caught it
	 with wait(), we mask out SIGCHLD so we don't try to leave this
	 stack frame later on. */

      WHAT (0, "finishing up");
      fflush (stream);

      if (pid)
	{
	  fclose (stream);
	  while (wait (NULL) != -1);
	  signal (SIGCHLD, SIG_IGN);
	}
    }

  alarm (0);

  signal (SIGALRM, SIG_IGN);
  signal (SIGCHLD, SIG_IGN);

  return 0;
}


/* Display last time user was seen */
int
display_last_seen (user, stream)
  char *user;
  FILE *stream;
{
  int f, i, packets_output;
  FINGER_PACKET **upackets;


  f = open (USERDATA, O_RDONLY, 0666);

  upackets = read_packets (f);

  if (f != -1)
    {
      close (f);
      sort_packets (upackets);
    }

  for (i = packets_output = 0; upackets[i]; i++)
    {
      if (!xstricmp (upackets[i]->name, user))
	{
	  show_unlogged_packet (upackets[i], stream);
	  packets_output++;
	  break;
	}
    }
  

  if (!packets_output)
    {
      for (i = 0; upackets[i]; i++)
	{
	  if (strindex (upackets[i]->real_name, user))
	    {
	      show_unlogged_packet (upackets[i], stream);
	      packets_output++;
	    }
	}
    }
  
  free_array (upackets);

  if (packets_output)
    fprintf (stream, "\r\n");

  return packets_output;
}


/* What to show in the case that a user is not logged in on any clients.
   Print information about the user owning PACKET to STREAM. */
show_unlogged_packet (packet, stream)
     FINGER_PACKET *packet;
     FILE *stream;
{
  char *the_time = (char *)ctime (&packet->idle_time);

  /* Plug the LF with a NUL */
  the_time[strlen(the_time)-1] = 0;

  fprintf (stream, "%s (%s) is not presently logged in.\n",
	   packet->real_name, packet->name);
  fprintf (stream, "Last seen at %s on %s\r\n", packet->host, the_time);
#if 0
  packet->idle_time = 0;
  print_packet (packet, stream);
#endif
}


/* Run special-target script for TARGET, if any, redirecting the
   output to STREAM. The request is of FINGER_TYPE, which is either 'l'
   (long), 's' (short), or 'x' (either). Returns a non-zero value if
   TARGET is a special target. The script is run with stdin set to
   /dev/null. */

int
maybe_special_target (target, stream, finger_type)
  char *target, finger_type;
  FILE *stream;
{
  struct stat statdata;
  pid_t pid = 0;
  char *target_script;


  if (!target || !stream)
    return 0;
  
  target_script = xmalloc (strlen (TARGETDIR) + 1 + 2 + strlen (target) + 1);

  sprintf (target_script, "%s/%c-%s", TARGETDIR, finger_type, target);
  
  if (stat (target_script, &statdata) >= 0)
    {
      signal (SIGPIPE, SIG_IGN);

      /* Create child */
      if (pid = vfork ())

	/* Ignore SIGCHLD */
	signal (SIGCHLD, SIG_IGN);

      else
	{
	  alarm (0);
	  signal (SIGALRM, SIG_IGN);

	  /* Make /dev/null be stdin */
	  close (0);
	  open ("/dev/null", O_RDONLY);

	  /* Make child stdout and stderr be the TCP stream */
	  if (fileno (stream) != 1)
	    {
	      close (1);
	      dup (fileno (stream));
	    }

	  if (fileno (stream) != 2)
	    {
	      close (2);
	      dup (fileno (stream));
	    }

	  /* Set default directory */
	  chdir (TARGETDIR);

	  /* Run ~/.fingerrc through user shell */
	  execlp ("/bin/sh", "/bin/sh", "-c", target_script, NULL);

	  fprintf (stream, "Exec failure - please contact postmaster.\r\n");
	  return 0;
	}

      while (wait (NULL) != -1);

      return !0;
    }

  return 0;
}
