/* in.fingerd.c -- Return information from the standard Finger port. */

/* 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 <netdb.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>

#include <fingerpaths.h>
#include <general.h>
#include <client.h>
#include <error.h>
#include <os.h>
#include <packet.h>
#include <util.h>
#include <getservhost.h>
#include <netinet/in.h>
#include <pwd.h>
#include <tcp.h>

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

/* If this is non-zero then all instances of users are reported on
   instead of having one unique entry for each user. */
int all_users = 0;

/* **************************************************************** */
/*								    */
/*		    INET Daemon Finger Replacement		    */
/*								    */
/* **************************************************************** */

#define IP 0			/* Internet Protocol */

/* A single line of arguments as read from the input stream. */
char *arguments = NULL;
int arg_size = 0;


/* Offset of the next available character in arguments. */
int next_char = 0;

static char *serverhost, *this_host;

/* Get args from stdin, and print info to stdout. */
main (argc, argv)
  int argc;
  char **argv;
{
  default_error_handling (argv[0]);

  /* Gobble line of arguments from user.  If the connection is already
     closed, then just exit. */
  if (getline (&arguments, &arg_size, stdin) < 0)
    exit (0);

  /* Are we the finger daemon running on the server host? */
  serverhost = getservhost (stdout);

  if (!serverhost)
    exit (1);

  if (!(this_host = xgethostname ()))
    this_host = "localhost";

  /* If this is the server host, then handle the request directly.
     Otherwise, open a connection to the server and pass the input
     and output back and forth. */

  if (host_cmp (this_host, serverhost))
    {
      char *arg, *next_argument ();
      void finger_reply ();

      arg = next_argument ();
      finger_reply (arg, stdout);
    }
  else
    client_finger (serverhost);

  free (arguments);
}

/* Chunk size for information transfers over the net. */
#define CHUNK 4096

/* As a client, we just have to ask the server for the information. */
client_finger (serverhost)
  char *serverhost;
{
  struct hostent *host;
  FILE *server;
  int connection, fd, i, local = 0;
  char *peek, *next_argument ();


  /* If this finger is ".local", then call the normal reply server
     directly, as if this machine was the server. */

  peek = next_argument ();

  if (peek)
    local = (xstricmp (peek, ".local") == 0);

  if (local)
    {
      /* Sun's compiler is an annoying f*ck... */
      void finger_reply ();

      finger_reply (peek, stdout);
      return;
    }
      
  host = gethostbyname (serverhost);

  if (!host)
    handle_error (FATAL, "can't lookup server host `%s'", serverhost);

  if (peek)
    {
      if (!xstricmp (peek, ".version"))
	{
	  printf ("GNU Finger server version %s, running on %s,\n  passing request to server at %s.\n",
		  VERSION_STRING, this_host, serverhost);
	  
	  /* Don't touch! */
	  fflush (stdout);
	}
    }

  connection = tcp_to_service (FINGER_TO_FINGER_SERVICE, (char *)host->h_addr);

  if (connection < 0)
    file_error (FATAL, host->h_name);

  fd = dup (connection);

  if ((server = fdopen (fd, "w")) == (FILE *)NULL)
    file_error (FATAL, host->h_name);

  /* Pass the arguments through, outputting the responses. */
  i = strlen (arguments);

  if (i && (arguments[i - 1] == '\n'))
    arguments[i - 1] = '\0';

  fprintf (server, "%s\r\n", arguments);
  fflush (server);

  /* Pass information in big chunks. */
  {
    byte *transfer_buffer;
    int bytes_transferred;

    transfer_buffer = (byte *)xmalloc (CHUNK);

    while ((bytes_transferred = read (fd, transfer_buffer, CHUNK)) > 0)
      write (fileno (stdout), transfer_buffer, bytes_transferred);

    free (transfer_buffer);
  }

  fflush (stdout);
  fclose (server);
  close (fd);
}


/* **************************************************************** */
/*                                                                  */
/*                 Getting and Parsing Argumements                  */
/*                                                                  */
/* **************************************************************** */

#define ADVANCE 1

/* Rewind argument parser */
static void
rewind_arguments ()
{
  next_char = 0;
}


char *next_arg_internal ();

/* Return the next argument available.   Return a new string which is
   the argument, or a pointer to NULL if no arguments remain. */
char *
next_argument ()
{
  return (next_arg_internal (ADVANCE));
}

/* Return the next argument available in arguments, but leave us
   still pointing at that argument. */
char *
peek_argument ()
{
  return (next_arg_internal (!ADVANCE));
}

/* Return the next argument available.   Return a new string which is
   the argument, or a pointer to NULL if no arguments remain.  If ADVANCE
   is non-zero, then advance the argument pointer to the following argument
   so that subsequent calls can get subsequent arguments. */
char *
next_arg_internal (advance)
     int advance;
{
  register int i;
  int start, end, len;
  char *arg;

  if (next_char >= strlen (arguments))
    return ((char *)NULL);

  i = next_char;

  /* Isolate argument. */
  while (arguments[i] && (whitespace (arguments[i]) || arguments[i] == ','))
    i++;

  start = i;

  while (arguments[i] && !whitespace (arguments[i]) && arguments[i] != ',')
    i++;

  end = i;

  if (start == end)
    return ((char *)NULL);

  arg = (char *)xmalloc ((len = end - start) + 1);
  strncpy (arg, &arguments[start], len);
  arg[len] = '\0';

  if (advance)
    next_char = end;

  return (arg);
}


/* **************************************************************** */
/*								    */
/*			How the main server replies.		    */
/*								    */
/* **************************************************************** */

/* A minute is 60 seconds. */
#define MINUTES(x) (x * 60)

/* How long the console of a machine has to be idle before we think it
   is free. */
#define FREE_IDLE_TIME MINUTES(60)

/* How long the console of a machine has to be idle in order to look like
   it might be free in the future.  This is used when answering the .clients
   message. */
#define IDLE_TIME MINUTES(3)

/* How many machines should be free for any given site?  Although a
   percentage of the available machines would be reasonable, I think
   that there should be a lower limit. */
#if !defined ENOUGH_FREE_MACHINES
#define ENOUGH_FREE_MACHINES 3
#endif

/* Generate output to STREAM for USER.  USER can be one of the following:

	username	Return the collected finger packets for USERNAME.

	/W		Return the long (database) information about the
			following USERNAME.

	.free		Return a list of free machines. (Those
			idle for a "long" time.)

	.all		Return the information about every machine that
			we know about, in a format that is useful.

	.version        Return the server version number and host name.
	
	.clients	Returns a list of the clients that this server
			serves.

	:user:		If the username is surrounded with colons, then
			this is a request for the user's face.  If we
			can find the data, then return that.

        .faces		Return a list of the available faces.

	.local		Finger only at this machine.
*/
static void list_clients ();

void
finger_reply (user, stream)
  char *user;
  FILE *stream;
{
  register int i, f, len;
  int match_free, match_all;
  int packets_output, face;
  FINGER_PACKET **hpackets, **upackets;
  CLIENT **get_clients ();
  void no_such_user ();
  int is_long_finger;

  packets_output = face = is_long_finger = 0;

  if (!user)
    user = "";

  if (!xstricmp (user, "/W"))
    {
      is_long_finger = 1;
      free (user);
      user = next_argument ();
    }

  /* ".version" means we print server version and host name we run on */
  if (!xstricmp (user, ".version")) 
   {
      printf ("GNU Finger server version %s, running on %s.\n",
	      VERSION_STRING, this_host);
      return;
    }

  if (*user == ':')
    {
      register char *t = user + 1;
      while (*t && *t != ':') t++;
      if (*t == ':')
	{
	  face = 1;
	  user++;
	  *t = '\0';
	}
    }

  /* If the requesting machine is expecting a face, then send it along
     if we have it. */
  if (face)
    {
      send_face (user, stream);
      return;
    }

  if (maybe_special_target (user, stream, 's')
      || maybe_special_target (user, stream, 'x'))
    return;

  /* Is this a local finger?  If so, just call the local client finger
     daemon to get packets, and print them out. */
  if (xstricmp (user, ".local") == 0)
    {
      upackets = get_finger_data (1);

      sort_packets (upackets);

      for (i = 0; upackets[i]; i++)
	ascii_packet (upackets[i], stream, i == 0);

      if (!upackets[0])
	fprintf (stream, "No one logged on.\r\n");

      free_array (upackets);
      return;
    }

  match_free = (xstricmp (user, ".free") == 0);
  match_all = ((xstricmp (user, ".all") == 0) || !*user);

  /* ".users" returns a listing of everyone in the userdata file. */
  if (xstricmp (user, ".users") == 0)
    {
      warn_if_not_recent (HOSTDATA, stream);

      f = open (USERDATA, O_RDONLY, 0666);

      if (f < 0)
	{
	  fflush (stream);
	  return;
	}

      upackets = read_packets (f);
      close (f);
      sort_packets (upackets);

      if (*upackets)
	{
	  for (i = 0; upackets[i]; i++)
	    fprintf (stream, "%s (%s) seen at %s on %s",
		     upackets[i]->real_name, upackets[i]->name,
		     upackets[i]->host, ctime (&upackets[i]->idle_time));
	}
      else
	{
	  fprintf (stream, "%s file is emtpy!\n", USERDATA);
	}

      fflush (stream);
      return;
    }

  /* ".clients" returns a listing of everyone in the hoststatus file. */
  if (!xstricmp (user, ".clients"))
    {
      list_clients (stream);
      return;
    }

  /* .faces returns a list of all of the available faces. */
  if (xstricmp (user, ".faces") == 0)
    {
      int faces_listed = site_list_faces (stream);

      fprintf (stream, "%d faces listed.\n", faces_listed);
      return;
    }

  /* Is this a `wide' (long) finger? */
  if (is_long_finger && user)
    {
      if (!long_finger (user, stream, 0))
	no_such_user (user, this_host, stream);

      return;
    }

  /* If the user wants to know about free machines then look the information
     up in the database kept on the availability of hosts.  Print statistics
     that seem strange, like if the file of host information hasn't been
     written in a while. */
  if (match_free)
    {
      int not_responding, free_hosts_index, total_clients;
      char **free_hosts = (char **)NULL;
      CLIENT **clients = (CLIENT **)NULL;

      clients = get_clients (stream);
      total_clients = array_len (clients);

      free_hosts = (char **)xmalloc (array_len (clients) * sizeof (char *));
      free_hosts_index = 0;
      not_responding = 0;

      for (i = 0; clients[i]; i++)
	{
	  if (clients[i]->client_up == 0)
	    {
	      not_responding++;
	    }
	  else
	    {
	      /* A host is free if it is up with no users, or if it has
		 been idle for at least as long as FREE_IDLE_TIME. */
	      if (clients[i]->users == 0 ||
		  clients[i]->idle_time >= FREE_IDLE_TIME)
		{
		  char *description = savestring (clients[i]->hostname);

		  /* If the reason this machine is free is because it
		     has been idle for a long time, then show how long
		     it has been idle in the description. */
		  if (clients[i]->users)
		    {
		      char *itime = idle_time_string (clients[i]->idle_time);
		      char *t;

		      t = (char *)xmalloc (1 + strlen (" (idle )") +
					   strlen (itime) +
					   strlen (description));

		      if (itime)
			{
		          sprintf (t, "%s (idle %s)", description, itime);
		          free (itime);
			}
		      else
			sprintf (t, "%s", description);
		      free (description);
		      description = t;
		    }
		  free_hosts[free_hosts_index++] = description;
		  packets_output++;
		}
	    }
	}
      free_hosts[free_hosts_index] = (char *)NULL;

      {
	/* If there aren't enough free machines, something is wrong, even
	   if it only means that this site doesn't have enough machines to
	   go around. */

	int percent_not_replied = 0, col = 0;
	int plural = packets_output != 1;

	/* Print a summary report. */
	fprintf (stream, "%d host%s free",
		 packets_output, packets_output == 1 ? "" : "s");

	if (not_responding)
	  fprintf (stream, ", %d not responding.\r\n", not_responding);
	else
	  fprintf (stream, ".\r\n");

	/* Print the list of free hosts. */

	if (packets_output)
	  {
	    fprintf (stream, "Free hosts are ");
	    col = strlen ("Free hosts are ");
	  }

	for (i = 0; free_hosts[i]; i++)
	  {
	    char *host;

	    host = sans_domain (free_hosts[i]);

	    if ((col + 2 + strlen (host)) > 79)
	      {
		fprintf (stream, "\r\n");
		col = 0;
	      }

	    fprintf (stream, "%s%s", host,
		     free_hosts[i + 1] ? ", " : ".\r\n");

	    col += 2 + strlen (host);

	    if (i + 2 == packets_output)
	      {
		if (col + strlen ("and ") > 79)
		  {
		    fprintf (stream, "\r\n");
		    col = 0;
		  }
		fprintf (stream, "and ");
		col += strlen ("and ");
	      }

	    free (host);
	  }

	free_array (free_hosts);

	/* Find out which percentage of the machines are not responding. */
	if (not_responding)
	  {
	    double x, x1, x2;
	    x1 = total_clients; x2 = not_responding;
	    x = x1 / x2;
	    x = 100 / x;
	    percent_not_replied = x;
	  }

	fprintf (stream, "\n");

	/* What's wrong?  Not enough machines? */
	if (percent_not_replied < 10)
	  {
	    if (packets_output < ENOUGH_FREE_MACHINES)
	      fprintf (stream, "\
There %s %d machine%s free, even though %d%% of the hosts are responding.\n\
Maybe you should ask whoever is in charge to purchase more machines,\n\
since there obviously aren't enough to go around.\n\
", plural? "are" : "is", packets_output,
			 plural? "s" : "", 100 - percent_not_replied);
	    }
	  else fprintf (stream, "\
There %s %d known free machine%s, but %d%% of all the hosts did not\n\
respond.  It is likely that for some reason those hosts are not\n\
running the appropriate daemon.  You might want to complain to the\n\
system administrator about this, since this program can only report\n\
the status of machines that it can talk to.\n\
", plural? "are" : "is", packets_output,
			plural? "s" : "", percent_not_replied);

	}
	return;
      }
  
  warn_if_not_recent (HOSTDATA, stream);

  f = open (HOSTDATA, O_RDONLY, 0666);

  hpackets = read_packets (f);

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

  f = open (USERDATA, O_RDONLY, 0666);

  upackets = read_packets (f);

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

  for (i = 0; hpackets[i]; i++)
    {
      if (match_all || (xstricmp (user, hpackets[i]->name) == 0))
	{
	  ascii_packet (hpackets[i], stream, packets_output == 0);
	  packets_output++;
	}
    }

  /* If no packets were output, and the caller wanted to match everybody,
     sorry, there is no one logged in anywhere that we know of.  But if the
     caller wanted a specific user, check the old user file for old logins.
     If it isn't found there, then try a match on the real_name field. */

  if (packets_output == 0)
    {
      if (match_all)
	{
	  fprintf (stream, "No one logged in on client machines.\n");
	  free_array (hpackets);
	  free_array (upackets);
	  return;
	}
      else if (!match_all && !match_free)
	{
	  for (i = 0; upackets[i]; i++)
	    {
	      if (xstricmp (upackets[i]->name, user) == 0)
		{
		  show_unlogged_packet (upackets[i], stream);
		  packets_output++;
		  break;
		}
	    }
	}

      /* If we haven't found any packets, then try real names.
	 Start with people who are currently logged in.
	 Then try the people who are not. */
      if (!packets_output)
	{
	  for (i = 0; hpackets[i]; i++)
	    if (matches_username (user, hpackets[i]->real_name))
	      {
		ascii_packet (hpackets[i], stream, !packets_output);
		packets_output++;
	      }

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

	      if (!packets_output)
		{
		  char *serverhost = getservhost (NULL);
		  struct passwd *pwent = getpwnam (user);

		  /* Finally, all else having failed, make sure user exists.
		     If so, then we just don't have any login information. */
		  if (pwent)
		    {
		      fprintf (stream, "\r\n%s (%s) has never logged on. For more information,\r\n",
			       pw_real_name (pwent), user);
		      fprintf (stream, "try `finger --info %s@%s' or `finger -l %s@%s'.\r\n\r\n",
			       user, this_host, user, this_host);

		      if (serverhost)
			free (serverhost);
		      return;
		    }
		  
		  /* User doesn't even exist. */
		  no_such_user (user, this_host, stream);
		  
		  if (serverhost)
		    free (serverhost);
		}
	    }
	}
    }

  free_array (hpackets);
  free_array (upackets);
}


/* List client info */
static void
list_clients (stream)
  FILE *stream;
{
  CLIENT **get_clients ();
  CLIENT **clients = get_clients (stream);
  char *idle_time, status[20];
  char *serverhost = getservhost (stream);
  int clients_count = array_len (clients);
  int plural = (clients_count != 1);
  int f, entry;
  int i;
  FINGER_PACKET **hpackets = NULL, **conusers = NULL, used_entry;
  char *host;
  

  fprintf (stream, "There %s %d client%s served by %s.\n\n",
	   plural? "are" : "is", clients_count, plural? "s" : "",
	   serverhost);
  
  fprintf (stream, "%-16s ", "Hostname");
  fprintf (stream, "%-15s ", "Address");
  fprintf (stream, "%-16s ", "Status");
  fprintf (stream, "Console user\r\n");
  fflush (stream);
  
  warn_if_not_recent (HOSTDATA, stream);
  
  f = open (HOSTDATA, O_RDONLY, 0666);
  
  hpackets = read_packets (f);
  
  if (f != -1)
    {
      close (f);
      /* sort_packets (hpackets); */
      conusers = console_users (hpackets);
    }
  
  for (i = 0; clients[i]; i++)
    {
      status[0] = '\0';
      
      if (clients[i]->client_up)
	{
	  if (clients[i]->users == 0)
	    strcpy (status, "free");
	  else
	    {
	      if (clients[i]->idle_time < IDLE_TIME)
		strcpy (status, "up");
	      else
		strcpy (status, "idle");
	      
	      sprintf (status + strlen (status), "(%d user%s)",
		       clients[i]->users,
		       clients[i]->users == 1 ? "" : "s");
	    }
	}
      else
	strcpy (status, "Not responding");
      
      /* UGH!!  We need a struct in_addr because of alignment hassles. */
      {
	struct in_addr addr;
	
	bcopy (&(clients[i]->address[0]), &addr.s_addr, 4);
	
	host = sans_domain (clients[i]->hostname);

	fprintf (stream, "%-16s ", host);
	fprintf (stream, "%-15s ", inet_ntoa (addr));
	fprintf (stream, "%-16s ", status);

	free (host);
      }
      
      /* Loop host data packets, and see if we can find who's logged onto
	 the console. */

      for (entry = 0; conusers[entry]; entry++)
	if (conusers[entry] != &used_entry
	    && host_cmp (conusers[entry]->host, clients[i]->hostname))
	  {
	    fprintf (stream, "%s", conusers[entry]->name);
	    conusers[entry] = &used_entry;
	    break;
	  }

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

  if (conusers)
    free (conusers);

  if (hpackets)
    free_array (hpackets);

  fflush (stream);
  return;
}


/* USER@HOST doesn't exist. Print message to this effect on STREAM,
   and send request to the forwardhost if any. */

void
no_such_user (user, host, stream)
  char *user, *host;
  FILE *stream;
{
  char *forwardhost = getforwardhost ();
  
  if (forwardhost)
    {
      fprintf (stream, "[No user %s@%s, forwarding\r\n request to %s]\r\n",
	       user, this_host, forwardhost);

      fflush (stream);

      rewind_arguments ();
      client_finger (forwardhost);

      free (forwardhost);
      return;
    }

  fprintf (stream, "\r\n");
  fprintf (stream, "There is no user named %s here; check your spelling. Feel free\r\n", user);
  fprintf (stream, "to contact postmaster@%s for further\r\n",
	   serverhost ? serverhost : this_host);
  fprintf (stream, "assistance in locating this person. You can also try to use part\r\n");
  fprintf (stream, "of the real name, if known. E.g., ``finger stallman@gnu.ai.mit.edu''.\r\n");
  fprintf (stream, "\r\n");
  fprintf (stream, "It is also quite possible that %s is a mail alias. In that case,\r\n", user);
  fprintf (stream, "you can finger it by giving the ``-l'' option to your finger program\r\n");
  fprintf (stream, "(or --info if you're using the GNU Finger client). Aliases are not\r\n");
  fprintf (stream, "looked up without this option due to the extra processing involved.\r\n");
  fprintf (stream, "\r\n");
}


/* Return a list of clients that this server serves. */
CLIENT **
get_clients (error_stream)
  FILE *error_stream;
{
  CLIENT **clients = (CLIENT **)NULL;
  CLIENT client;
  int clients_index, clients_size;
  int fd;

  warn_if_not_recent (HOSTSTAT, error_stream);

  fd = open (HOSTSTAT, O_RDONLY, 0666);

  if (fd < 0)
    file_error (FATAL, HOSTSTAT);

  clients_index = 0;
  clients_size = 0;

  while ((read (fd, &client, sizeof (client))) == sizeof (CLIENT))
    {
      if (clients_index + 1 >= clients_size)
	{
	  if (!clients)
	    clients = (CLIENT **)
	      xmalloc ((clients_size = 20) * sizeof (CLIENT *));
	  else
	    clients = (CLIENT **)
	      xrealloc (clients, (clients_size += 20) * sizeof (CLIENT *));
	}

      clients[clients_index] = (CLIENT *)xmalloc (sizeof (CLIENT));
      bcopy (&client, clients[clients_index], sizeof (CLIENT));
      clients[++clients_index] = (CLIENT *)NULL;
    }
  close (fd);

  if (!clients)
    {
      if (error_stream)
	fprintf (error_stream, "Unable to read host data from %s\n",
		 HOSTSTAT);
      exit (1);
    }

  return (clients);
}

/* Simply complain if FILENAME hasn't been written in a while.  This can be
   an indication that the finger server has wedged or otherwise stopped
   running.  Return non-zero if the file hasn't been written recently. */
int
warn_if_not_recent (filename, error_stream)
     char *filename;
     FILE *error_stream;
{
  int result = 0;
  struct stat finfo;

  if (stat (filename, &finfo) != -1)
    {
      long last_change = time ((long *)0) - finfo.st_ctime;

      if (last_change > MINUTES (10))
	{
	  char *itime = idle_time_string (last_change);

	  fprintf (error_stream, "%s: file has not changed in %s\n",
		   filename, itime ? itime : "");

	  if (itime)
	    free (itime);
	  result = 1;
	}
    }
  else
    {
      fprintf (error_stream, "%s: file cannot be stat'ed!\n", filename);
      result = 1;
    }

  fflush (error_stream);
  return (result);
}
