/* fingerd.c -- Poll machines for information on who is using them. */

/* 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 <config.h>
#include <stdio.h>
#include <fcntl.h>
#include <setjmp.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/signal.h>

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

#include "getopt.h"

void *xmalloc (), *xrealloc ();

/* Print trace message to stdout - used for debugging the code */
#define TRACES 1

#if TRACES
#define TRACE(MSG)  \
{ extern char *ctime(); \
    if (trace) { long t; char *ts; time(&t); ts = ctime (&t); ts[strlen(ts)-1] = 0; \
		 printf ("fingerd %s:", ts); printf MSG; putchar ('\n'); fflush (stdout); } }
#else
#define TRACE(FOO)
#endif

/* Trace flag */
#if TRACES
volatile int trace = 0;
#endif

/* The pathname of the list of clients to poll.  You can override the
   default with the environment variable FINGERCLIENTS. */
char *finger_clients;

/* The database containing info from all polled hosts. */
char *hostdata;

/* The database containing info about specific users. */
char *userdata;

/* The database of the status of polled clients. */
char *hoststat;

/* The amount of time (in seconds) we sleep between each poll. */
volatile int time_between_polls = DEFAULT_POLL_INTERVAL;

/* The number of machines polled each time. */
int poll_count = 100;

/* We keep the database of inet numbers in core.  This appears to be the
   most effecient use of available resources. */

/* Array of clients.  Each entry contains a HOSTENT, as returned
   by gethostbyaddr (), and a HOSTSTATUS.  The list is terminated
   with a NULL pointer. */
CLIENT **clients = (CLIENT **)NULL;

/* Offset into the clients array. */
volatile int client_index;

/* Number of slots created for the CLIENTS array. */
int clients_size = 0;

/* Zonzero if this process has received a SIGHUP. Initially set to
   nonzero to cause a recaching of the client tables. */
volatile int caught_sighup;

/* Flag variables. */

/* Non-zero means report on all users, even if the same one appears twice. */
int all_users = 0;

/* Non-zero means print debugging info. */
int debugging = 0;

/* Non-zero means don't fork daemon into background */
int nofork = 0;

/* The output stream for debugging. */
FILE *debug_output = stderr;

/* Our in RAM database of user/host mappings. */
FINGER_PACKET **host_packets = (FINGER_PACKET **)NULL;

/* Number of slots allocated to the above array. */
int host_packets_size = 0;

struct option longopts[] =
{{ "debug", 0, 0, 'd' },
   { "all", 0, 0, 'a' },
   { "all-users", 0, 0, 'a'},
   { "nofork", 0, 0, 'f' },
   { "interval", 0, 0, 'i' },
#if TRACES
   { "trace", 0, 0, 't'},
#endif
   { 0, 0, 0, 0 }};


/* The daemon which polls clients. */
main (argc, argv)
     int argc;
     char **argv;
{
  extern int allow_time_outs;
  char *serverhost, *this_host;
  int arg_index = 1, optc;


  allow_time_outs = 1;

  default_error_handling (argv[0]);

  /* Parse arguments. */
  while ((optc = getopt_long (argc, argv, "datfi", longopts, NULL)) >= 0)
    switch (optc)
      {
      case 'd':

	debugging = 1;
	break;

#if TRACES
      case 't':

	trace = 1;
	break;
#endif
      case 'f':
	nofork = 1;
	break;

      case 'a':
	break;

      case 'i':
	if ((time_between_polls = atoi (optarg)) < 0)
	  time_between_polls = DEFAULT_POLL_INTERVAL;
	break;
	
      default:
	usage ();
      }
  

  serverhost = getservhost (stderr);
  if (!serverhost)
    exit (1);

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

  if (!host_cmp (serverhost, this_host))
    handle_error (FATAL, "%s is not the Finger server.  %s is.",
		  this_host, serverhost);

  if (!debugging)
    {
      pid_t pid;

      pid = (nofork ? getpid() :  fork ());

      if (pid)
	{
	  if (pid == -1)
	    handle_error (FATAL, "Can't fork a child!");

	  printf ("%s: daemon starting (pid %d)\n", argv[0], pid);

	  if (!nofork)
	    exit (0);
	}

      if (trace)
	fprintf (stderr, "%s: trace messages will be printed to stdout\r\n", argv[0]);

      if (!nofork)
#if defined (TIOCNOTTY)
	/* Turn off the controlling terminal in the child. */
	{
	  int tty = open ("/dev/tty", O_RDWR, 0666);
	  
	  if (tty)
	    ioctl (tty, TIOCNOTTY, 0);
	  
	  close (tty);
	}
#else
        setpgrp ();
#endif /* !TIOCNOTTY */
    }
  else
    {
      allow_time_outs = 0;
    }

  /* Initialize the data base and defaults. */
  initialize_server ();

  for (;;)
    {
      extern jmp_buf top_level;

      if (setjmp (top_level))
	{
	  clean_up_for_exit ();
	  exit (2);
	}
      if (caught_sighup)
	{
	  initialize_clients ();
	  caught_sighup = 0;
	}

      poll_some_clients ();

      TRACE (("going to sleep for %u seconds", time_between_polls));
      sleep (time_between_polls);
    }
}

/* What to do just before abnormal exit. */
clean_up_for_exit ()
{
}

/* Tell people how to start the finger daemon. */
usage ()
{
  extern char *progname;

  fprintf (stderr, "usage: %s [-d] [--debug] [-a] [--all]\n", progname);
  fprintf (stderr, "              [-i seconds] [--interval seconds]\n");
  exit (1);
}

/* Initialize the server. */
initialize_server ()
{
  void new_fingerd_clients ();

  TRACE (("initializing server -"));
  initialize_pathnames ();
  initialize_clients ();
  initialize_fingerdata ();
    
  if (!host_packets)
    {
      host_packets =
	(FINGER_PACKET **)xmalloc ((host_packets_size = 40)
				   * sizeof (FINGER_PACKET *));
      host_packets[0] = (FINGER_PACKET *)NULL;
    }

  signal (SIGHUP, new_fingerd_clients);
}

/* What to do when the daemon gets a SIGHUP signal. */
void
new_fingerd_clients ()
{
  TRACE (("---"));
  TRACE (("caught SIGHUP, rehashing client data -"));
  caught_sighup = 1;
}

/* Set global pathname variables which specify the location of 
   the client data, the clients to poll, etc. */
initialize_pathnames ()
{
  char *getenv ();

  TRACE(("  initializing pathnames"));

  finger_clients = getenv ("FINGERCLIENTS");

  if (!finger_clients)
    finger_clients = FINGERCLIENTS;

  hostdata = HOSTDATA;
  userdata = USERDATA;
  hoststat = HOSTSTAT;
}

/* Initialize the list of pollable clients from finger_clients.
   Called by fingerd_initialize (), and when the daemon gets a SIGHUP. */
int
initialize_clients ()
{
  TRACE (("  initializing clients"));
  initialize_clients_internal (finger_clients);
}

/* Initialize the list of pollable clients from CLIENTFILE.

   The file contains specifications of clients to poll.  The
   specifications may be in one of the following forms:

	a) The name of an internet host.
	b) The internet address of a host.
	c) A subnet specification, preceded by the keyword ":subnet".
	   You should be able to do ranges of machines with this,
	   but the only syntax that I can think of looks like:

	      :subnet 128.111.47.0 - 10

	   which would specify the machines from .0 to .10.
	d) A comment, which is a line whose first character is
	   a pound sign.  The comment ends at the end of the line.
	e) An at sign ("@") prepended before a port number or service
	   name, followed by a, b, or c, above. For example:

	     @2009 :subnet 128.111.47.0 - 10

	   Default is "@cfinger". */
int
initialize_clients_internal (clientfile)
  char *clientfile;
{
  register int i;
  int line_number = 0;
  char *temp, *line = NULL;
  int line_size = 0;
  char *directive = NULL, *hostname = NULL;
  char *port = NULL;
  FILE *stream;
  

  if ((stream = fopen (clientfile, "r")) == (FILE *)NULL)
    file_error (FATAL, clientfile);

  if (clients)
    {
      for (i = 0; clients[i]; i++)
	{
	  free (clients[i]);
	}

      free (clients);
      clients = (CLIENT **)NULL;
      clients_size = 0;
    }
    
  client_index = 0;

  while (getline (&line, &line_size, stream) >= 0)
    {
      line_number++;
      i = 0;
      if (directive)
	free (directive);

      if (port)
	free (port);

      directive = xmalloc (line_size + 1);
      port = xmalloc (line_size + 1);

      *directive = *port = 0;
      
      while (line[i])
	{
	  /* Skip to first character. */
	  while (line[i] && (line[i] == ',' || whitespace (line[i])))
	    i++;

	  /* If comment or line end, skip to the end and ignore. */
	  if (!line[i] || line[i] ==  '#')
	    {
	      i = strlen (line);
	      continue;
	    }

	  /* If special directive, do what it says. */
	  switch (line[i])
	    {
	    case ':':
	      {
		int start = ++i;
		
		/* Find end of directive. */
		while (line[i] && !whitespace (line[i]))
		  i++;
		
		directive[0] = '\0';
		if ((i - start) > 0)
		  {
		    strncpy (directive, line + start, i - start);
		    directive[i - start] = '\0';
		  }
		
		if (strcmp (directive, "subnet") == 0)
		  {
		    warning ("Can't hack subnets yet");
		    i = strlen (line);
		  }
		else
		  {
		    warning ("Bad directive `%s', in line %d of %s",
			     directive, line_number, clientfile);
		    i = strlen (line);
		  }
		continue;
	      }
	      break;

	    case '@':
	      {
		int start = ++i;

		/* Find end of port # or service */
		while (line[i] && !whitespace (line[i]))
		  i++;
		
		port[0] = '\0';
		if ((i - start) > 0)
		  {
		    strncpy (port, line + start, i - start);
		    port[i - start] = '\0';
		  }
		continue;
	      }
	      break;
	    }
	      
	  /* This is a regular host specification.  It is either an internet
	     address, or a hostname. */
	  {
	    struct hostent *hostinfo;
	    int start = i;

	    if (hostname)
	      free (hostname);

	    hostname = xmalloc (line_size + 1);

	    /* Isolate the host object. */
	    while (line[i] && !whitespace (line[i]) && line[i] != ',')
	      i++;

	    strncpy (hostname, &line[start], i - start);
	    hostname[i - start] = '\0';

	    /* Is this an internet address in dot notation? */
	    if (digit (hostname[0]))
	      {
		long address = (long)inet_addr (hostname);
		
		/* If malformed address, it still could be a name. */
		if (address == -1)
		  goto lookup_hostname;

		hostinfo = gethostbyaddr (&address, 4, AF_INET);
	      }
	    else
	      {
	      lookup_hostname:
		hostinfo = gethostbyname (hostname);
	      }

	    if (!hostinfo)
	      warning ("Can't get info for host `%s'", hostname);
	    else
	      add_client (hostinfo, port);
	  }
	}
    }

  if (line)
    free (line);

  if (directive)
    free (directive);

  if (hostname)
    free (hostname);

  if (port)
    free (port);

  fclose (stream);
  client_index = 0;
}


/* Add HOST to client list. The way to connect to cfinger on it is via
   PORT_OR_SERVICE, or by the default means if NULL. */
int
add_client (host, port_or_service)
  struct hostent *host;
  char *port_or_service;
{
  CLIENT *new;


  if (client_index + 2 >= clients_size)
    {
      if (clients == (CLIENT **)NULL)
	{
	  clients =
	    (CLIENT **)xmalloc ((clients_size = 20) * sizeof (CLIENT *));
	}
      else
	{
	  clients = (CLIENT **)xrealloc
	    (clients, (clients_size += 20) * sizeof (CLIENT *));
	}
    }

  new = (CLIENT *) xmalloc (sizeof (CLIENT));
  bzero (new, sizeof *new);

  strncpy (new->hostname, host->h_name, sizeof new->hostname);
  bcopy (host->h_addr, new->address, 4);
  new->client_up = 0;
  new->users = 0;
  new->idle_time = 0;
  new->times_polled = 0;

  if (port_or_service && *port_or_service)
    strncpy (new->port, port_or_service, sizeof new->port);

  clients[client_index++] = new;

  clients[client_index] = (CLIENT *)NULL;
}

/* Make sure that the files named in HOSTDATA and USERDATA  exist. */
initialize_fingerdata ()
{
  TRACE (("  initializing fingerdata"));
  force_existence (hostdata);
  force_existence (userdata);
}

/* Ensure that FILE exists, and is writable.  Don't change
   the file if it exists, but create it if it doesn't. */
force_existence (file)
     char *file;
{
  int fd = open (file, O_RDWR, 0666);

  if (fd < 0)
    {
      fd = open (file, O_CREAT | O_WRONLY, 0666);

      if (fd < 0)
	file_error (FATAL, file);
    }
  close (fd);
}

/* Poll POLL_COUNT hosts from CLIENTS, starting at CLIENT_INDEX.
   Leave CLIENT_INDEX pointing at the next host to poll. */
poll_some_clients ()
{
  register int i, j;
  CLIENT *client;

  for (i = 0, j = client_index; i < poll_count; i++)
    {
      client = clients[j];

      /* If we have reached the end of the list, wrap around and start
	 at the top again.  If the list is shorter than POLL_COUNT, then
	 just reset to start of list. */
      if (!client)
	{
	  if (client_index == 0)
	    {
	      j = 0;
	      break;
	    }
	  else
	    {
	      client_index = j = 0;
	      continue;
	    }
	}
      else
	j++;

      poll_client (client);
  }

  client_index = j;

  /* After doing a set of hosts, save the information on disk.  This is
     the way that we pass information from the poller to the reply server. */
  write_packets (host_packets, hostdata);
  write_host_status ();
}

/* Poll a single CLIENT.  Add information to the global data
   base of logged in users. */
poll_client (client)
  CLIENT *client;
{
  FINGER_PACKET **get_finger_data (), **finger_at_address ();
  register FINGER_PACKET **packets;
  register int i;


  TRACE (("---"));
  TRACE (("inquiring client `%s' about user `%s' [port: %s]",
	  client->hostname, "", client->port && *client->port ? client->port : CFINGER_SERVICE));

  packets = finger_at_address (client->address, "", client->port);

  /* Assume poll was successful. */
  client->client_up = 1;

  /* Since we have just polled this client, the information is more
     current than anything that we have saved.  So remove everything
     about this client from the list. */
  if (packets && packets[0])
    {
      remove_host_packets (host_packets, packets[0]->host);
      client->times_polled++;
    }
  else
    {
      /* Either there was an error from the host, or there is no one
	 logged on.  So we remove the entries about this host. */
      remove_host_packets (host_packets, client->hostname);
      client->users = 0;

      /* If there was an error, then this client is down. */
      if (!packets)
	client->client_up = 0;
    }

  /* Remember the information about each user at this machine. */
  if (packets)
    {
      unsigned long machine_idle_time = (unsigned long)-1;
 
      for (i = 0; packets[i]; i++)
	{
	  if (debugging)
	    print_packet (packets[i], debug_output);

	  if (packets[i]->idle_time < machine_idle_time)
	    machine_idle_time = packets[i]->idle_time;

	  record_user_info (packets[i]);
	}

      client->users = i;
      client->idle_time = machine_idle_time;
      free_array (packets);
    }

  TRACE (("  (client `%s' poll done)", client->hostname));

}

/* **************************************************************** */
/*								    */
/*		     Keeping Track of User/Host			    */
/*								    */
/* **************************************************************** */

/* Record the finger information found in PACKET in our database of
   such packets. */
record_user_info (packet)
  FINGER_PACKET *packet;
{
  FINGER_PACKET check;
  int packet_found = 0;
  register int i;


  /* If the user is in this database as one who is on this host
     already, overwrite the entry with the current one, since it is
     more up to date.  Only allow console user records to be
     overwritten by more recent console user info. */

  for (i = 0; host_packets[i]; i++)
    {
      if (!strcmp (host_packets[i]->name, packet->name)
	  && !strcmp (host_packets[i]->host, packet->host)
	  && !(is_console (host_packets[i]->ttyname)
	       && !is_console (packet->ttyname)))
	{
	  if (host_packets[i]->idle_time >= packet->idle_time)
	    bcopy (packet, host_packets[i], sizeof (FINGER_PACKET));
	  
	  packet_found++;
	  break;
	}
    }

  /* If the user/host wasn't found, then add this entry to the list. */

  if (!packet_found)
    {
      if (i + 2 >= host_packets_size)
	{
	  host_packets =
	    (FINGER_PACKET **)xrealloc (host_packets,
					((host_packets_size *= 2) + 1)
					* sizeof (FINGER_PACKET *));
	  TRACE ((">>> Host packet table expanded to %d slots\n", host_packets_size));
	}

      host_packets[i] = (FINGER_PACKET *)xmalloc (sizeof (FINGER_PACKET));
      bcopy (packet, host_packets[i], sizeof (FINGER_PACKET));
      host_packets[i + 1] = (FINGER_PACKET *)NULL;
    }

  /* In any case, update the disk file which contains the last known
     user locations. */
  {
    int file;
    long offset = 0;
    FINGER_PACKET check;

    file = open (userdata, O_RDWR, 0666);

    if (file < 0)
      file = open (userdata, O_CREAT | O_RDWR, 0666);

    if (file < 0)
      file_error (FATAL, userdata);

    packet_found = 0;

    TRACE (("  updating database `%s', record is -", userdata));
    TRACE (("    user=`%s', host=`%s', ttyname=`%s', what=`%s'",
	    packet->name, packet->host, packet->ttyname, packet->what));

    while (read (file, &check, sizeof (check)) == sizeof (check))
      {
	if ((strcmp (check.name, packet->name) == 0))
	  {
	    packet_found++;
	    if (check.login_time <= packet->login_time)
	      {
		lseek (file, offset, L_SET);
		packet->idle_time = (long)time ((long *)NULL);
		write (file, packet, sizeof (FINGER_PACKET));
	      }
	    break;
	  }
	else
	  offset += sizeof (FINGER_PACKET);
      }
  
    if (!packet_found)
      {
	packet->idle_time = (long)time ((long *)NULL);
	write (file, packet, sizeof (FINGER_PACKET));
      }

    close (file);

    TRACE (("  (record written)"));
  }
}


/* Like HOST_CMP, but compares host in PACKET against HOST */
int
packet_host_cmp (packet, host)
  FINGER_PACKET *packet;
  char *host;
{
  return (host_cmp (packet->host, host));
}


/* Remove all entries from the LIST which are from HOST. */
remove_host_packets (list, host)
  FINGER_PACKET **list;
  char *host;
{
  remove_packets (list, packet_host_cmp, host);
}

/* Write out the contents of the CLIENTS array.  This lets us keep track
   of the state of the clients that are being polled. */
write_host_status ()
{
  register int i, file;

  file = open (hoststat, O_WRONLY | O_CREAT | O_TRUNC, 0666);

  if (file < 0)
    {
      file_error (WARNING, hoststat);
      return;
    }

  if (clients)
    {
      for (i = 0; clients[i]; i++)
	if (write (file, clients[i], sizeof (CLIENT)) == -1)
	  file_error (WARNING, hoststat);
    }

  close (file);
}

