/*
    net.m  Networking utility functions for cheezmud.
    Copyright (C) 1995  David Flater.

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

/*  Networking utility functions.  Some code fragments trace their origins
 *  through several generations back to LPMud by Lars Pensj|.
 *
 *  This is an expurgated version of the code I used for Unetd, with the high
 *  level functions redone to support the mud.  The low level functions have
 *  been working for a very long time and I have no intention of modifying them
 *  for reasons of aesthetics or efficiency.
 */

#include "net.h"
#include "cheezmud.h"

struct conlist *
findcon ();
void
flush_write ();
void
cat_text_file ();
void
lost_player ();

int insocket=-1, nfds=-1;
fd_set fdset, wrset, tfdset, twrset;
static struct conlist *cons = NULL;
long messbufsize;
char *messbuf;

void *
safmalloc (size_t bytes)
{
  void *a;
  assert (a = (void *) malloc (bytes));
  return a;
}

void
append_to_queue (char **buff, long *length, char *newdata, long newlength, long *memlength)
{
  if (*buff == NULL)
  {
    assert (!(*length));
    *buff = (char *) safmalloc (newlength);
    *length = newlength;
    memcpy (*buff, newdata, newlength);
  }
  else
  {
    *length += newlength;
    if (*memlength < *length)
    {
      /* Doing lots of reallocs seems to be a bad thing.  This should reduce
       * the number of reallocs.
       */
      *memlength = *length << 1;
      assert ((*buff = (char *) realloc (*buff, *memlength)));
    }
    memcpy (*buff + *length - newlength, newdata, newlength);
  }
}

/* Remove data from the front of a queue */
void
shorten_queue (char **buff, long *length, long remove_amount, long *memlength)
{
  assert (remove_amount <= *length);
  *length -= remove_amount;
  if (*length)
    memcpy (*buff, (*buff)+remove_amount, *length);
  else
  {
    free (*buff);
    *buff = NULL;
    *memlength = 0;
  }
}

/* Get rid of trailing CR's and LF's */
char *
noeol (char *s)
{
  char *a;
  a = &(s[strlen(s)]);
  while (a > s)
    if (*(a-1) == '\n' || *(a-1) == '\r')
      *(--a) = '\0';
    else
      break;
  return s;
}

char *
get_password (char *player)
{
  FILE *fp;
  static char passwd[80];
  char buf[80], buf2[80];
  assert (fp = fopen ("etc/passwd", "rt"));
  while (fgets (buf, 80, fp))
  {
    assert (sscanf (buf, "%s", buf2) == 1);
    if (!strcmp (buf2, player))
    {
      fclose (fp);
      strcpy (passwd, buf+strlen(buf2)+1);
      return (noeol (passwd));
    }
  }
  fclose (fp);
  return NULL;
}

/************* Message Handlers ************/

void
bitbucket ();
void
newhandler ();
void
authhandler ();
void
playerhandler ();

/* The /dev/null of message handlers.  Connections being deleted use this. */
void
bitbucket (char *buf, struct conlist *t)
{
  ;
}

void
newhandler (char *buf, struct conlist *t)
{
  if (buf[0] == '\0')
  {
    text_sock_write (t, "login: ");
    return;
  }
  t->name = strdup (buf);
  t->type = auth;
  t->handler = authhandler;
  text_sock_write (t, "password: ");
}

void
authhandler (char *buf, struct conlist *t)
{
  struct conlist *u;
  char *p;
  char temp[160];
  if (!(p = get_password (t->name)))
  {
    text_sock_write (t, "Wrong.\n");
    delmark (t);
    sprintf (temp, "Bad login from %s:  %s/%s", t->hname, t->name, buf);
    cheezlog (temp);
    return;
  }
  if (strcmp (p, buf))
  {
    text_sock_write (t, "Wrong.\n");
    delmark (t);
    sprintf (temp, "Bad login from %s:  %s/%s", t->hname, t->name, buf);
    cheezlog (temp);
    return;
  }
  u = findcon (t->name);
  if (u)
  {
    text_sock_write (t, "Usurping existing character.\n");
    text_sock_write (u, "Your character has been usurped.\n");
    t->mudplayer = u->mudplayer;
    u->mudplayer = NULL;
    delmark (u);
    [t->mudplayer setty: t];
  }
  else
  {
    if (strcmp (t->name, mudadmin)) {
      t->mudplayer = [Player new];
      t->maxidle = 3600;
    }
    else {
      t->mudplayer = [Mudadmin new];
      t->maxidle = 0;
    }
    [[[t->mudplayer setmudname: t->name] setty: t] load];
  }
  t->type = player;
  t->handler = playerhandler;
  t->notify = lost_player;
  text_sock_write (t, "> ");
  sprintf (temp, "%s logged in from %s", t->name, t->hname);
  cheezlog (temp);
}

void
playerhandler (char *buf, struct conlist *t)
{
  if (strlen(buf) > 80)
    buf[80] = '\0';
  if (buf[0] != '\0') {
    if (!([t->mudplayer ohce: buf]))
      text_sock_write (t, "Can't do that.\n");
  }
  text_sock_write (t, "> ");
}

/************* Loss Notification Handlers ************/

void
no_notify (struct conlist *t)
{
  ;
}

void
lost_player (struct conlist *t)
{
  if (t->mudplayer)
    [t->mudplayer logout];
}

/***************/

/* Mark a connection for deletion.  It's added to wrset to insure that the
 * deletion will occur in a timely fashion.
 */
void
delmark (struct conlist *t)
{
  t->type = delete;
  t->handler = bitbucket;
  FD_CLR (t->con, &fdset);
  FD_SET (t->con, &wrset);
}

/* Initialize the conlist structure for new connections and add to conlist.
 * Fields which are very connection-dependent are not set here.
 */
void
init_conlist (struct conlist *temp)
{
  temp->next = cons;
  cons = temp;
  temp->type = new;
  temp->handler = newhandler;
  temp->inbuffer = temp->yettosend = NULL;
  temp->inbuflen = temp->inmemsize = temp->sendlen = temp->sendmemsize = 0;
  temp->notify = no_notify;
  temp->lastrecv = time(NULL);
  temp->maxidle = 60;
  temp->name = NULL;
  temp->mudplayer = NULL;
}

/* Set options for most sockets we will use.
 *   -- Non-blocking I/O
 *   -- Keep connections alive
 */
void
sockopt (int socket)
{
  /* struct protoent *tcp; */
  int tmp = 1;

  /* Set non-blocking I/O. */
#if 0
  assert (ioctl(socket, FIONBIO, &tmp) != -1);
#endif
#ifdef __hpux
  assert (fcntl (socket, F_SETFL, O_NONBLOCK) != -1);
#else
  assert (fcntl (socket, F_SETFL, O_NDELAY) != -1);
#endif

  /* Socket-level option to keep connections "warm" */
  tmp = 1;
  if (setsockopt (socket, SOL_SOCKET, SO_KEEPALIVE, (char *) &tmp,
  sizeof (tmp)))
  {
    perror ("setsockopt");
    assert (0);
  }
}

void
powerup ()
{
  struct sockaddr_in sin;
  int tmp;

  sig_setup ();
  messbufsize = 1000;
  messbuf = safmalloc (messbufsize);
  FD_ZERO (&fdset);
  FD_ZERO (&wrset);

  memset ((char *) &sin, '\0', sizeof sin);
  sin.sin_family = (short)AF_INET;
  sin.sin_addr.s_addr = INADDR_ANY;
  assert ((insocket = socket (AF_INET, SOCK_STREAM, 0)) != -1);
  tmp = 1;
  if (setsockopt (insocket, SOL_SOCKET, SO_REUSEADDR,
    (char *) &tmp, sizeof (tmp)))
  {
    perror ("setsockopt");
    assert (0);
  }

  sin.sin_port = htons (inport_number);
  assert (bind (insocket, (struct sockaddr *)(&sin), sizeof sin) != -1);
  sockopt (insocket);
  assert (listen (insocket, 5) != -1);
  FD_SET (insocket, &fdset);
  nfds = max (insocket, nfds);
}

int
get_message (char **messbuf, long *size, struct conlist *t)
{
  long l, m;
  char *buff;
  buff = *messbuf;

  if (t->maxidle)
    if (time(NULL) - t->lastrecv > t->maxidle) {
      text_sock_write (t, "You snooze, you lose.\n");
      return -1;
    }

  /* Try to read new data. */
  unbreak_pipe ();
  l = read (t->con, buff, (*size)-1);
  if (broke_pipe ())
  {
    return -1;
  }
  if (l <= -1)
  {
    if (errno != EWOULDBLOCK && errno != EAGAIN)
    {
      fflush (stdout);
      perror ("read");
      fflush (stderr);
      return -1;
    }
    l = 0;
  }
  else if (!l)
  {
    return -1;
  }
  else
    t->lastrecv = time(NULL);
  buff[l] = '\0';

  /* Append new packet to buffered data */
  if (l)
    append_to_queue (&(t->inbuffer), &(t->inbuflen), buff, l, &(t->inmemsize));

  /* Try to return messages */
  if ((m = (int) memchr (t->inbuffer, '\n', t->inbuflen)))
  {
    m = m - (int)(t->inbuffer) + 1;
    if (m >= *size)
    {
      *size = m+1;
      assert (*messbuf = (char *) realloc (*messbuf, *size));
    }
    buff = *messbuf;
    memcpy (buff, t->inbuffer, m);
    buff[m] = '\0';
    noeol (buff);
  }

  /* Remove received data from the input buffer */
  if (m)
  {
    shorten_queue (&(t->inbuffer), &(t->inbuflen), m, &(t->inmemsize));
    return 1;
  }

  /* Sorry, no donuts today. */
  return 0;
}

/* Write raw data (no translations) onto a socket, or queue it up at least. */
void
bin_sock_write (struct conlist *t, char *data, long len)
{
  append_to_queue (&(t->yettosend), &(t->sendlen), data, len,
    &(t->sendmemsize));
  flush_write (t);
}

/* Translate EOLs */
void
text_sock_write (struct conlist *t, char *buff)
{
  char *buff2;
  long length, from, to;

  length = strlen (buff);
  buff2 = (char *) safmalloc (length*2+1);

  for (from=0,to=0; buff[from] != '\0';)
  {
    if (buff[from] == '\n')
    {
      /* Avoid duplication */
      if (to == 0)
        buff2[to++] = '\r';
      else if (buff2[to-1] != '\r')
        buff2[to++] = '\r';
    }
    buff2[to++] = buff[from++];
  }
  buff2[to] = '\0';      /* Just in case */
  bin_sock_write (t, buff2, to);
  free (buff2);
  return;
}

/* Check for new connections and accept them. */
int openfiles=0;
void
accept_new_cons ()
{
  int fd;
  struct sockaddr addr;
  int length = sizeof addr;
  if ((fd = accept (insocket, &addr, &length)) == -1)
  {
    if (errno != EWOULDBLOCK && errno != EAGAIN)
    {
      if (errno == EMFILE || errno == ENFILE)
      {
        if (!openfiles)
        {
          openfiles = 1;
          fflush (stdout);
          perror ("accept");
          fflush (stderr);
          puts ("^^^^ This error will only be reported once to avoid filling the log file...");
        }
      }
      else
      {
        fflush (stdout);
        perror ("accept");
        fflush (stderr);
      }
    }
  }
  else
  {
    struct hostent *hp;
    struct conlist *temp;
    struct sockaddr_in *u = (struct sockaddr_in *)(&addr);
    unsigned long a = u->sin_addr.s_addr;
    char temp2[160];
    sockopt (fd);
    temp = (struct conlist *) safmalloc (sizeof (struct conlist));
    init_conlist (temp);
    FD_SET (fd, &fdset);
    nfds = max (fd, nfds);
    temp->con = fd;

    if (!(hp = gethostbyaddr ((const char *)(&a), sizeof a, AF_INET)))
      strcpy (temp->hname, inet_ntoa (u->sin_addr));
    else
      strcpy (temp->hname, hp->h_name);

    sprintf (temp2, "Connection from %s", temp->hname);
    cheezlog (temp2);

    /* Say Hi */
    cat_text_file (temp, "etc/motd");
    text_sock_write (temp, "login: ");

    /* See if there are any more waiting */
    accept_new_cons ();
  }
}

/* Close and destroy a connection */
void
remove_con (struct conlist *delme)
{
  struct conlist *p, *t;
  if (cons->con == delme->con)
    cons = delme->next;
  else
  {
    p = cons;
    t = p->next;
    while (t)
    {
      if (t->con == delme->con)
      {
        p->next = t->next;
        break;
      }
      p = t;
      t = t->next;
    }
    assert (t);
  }
  FD_CLR (delme->con, &fdset);
  FD_CLR (delme->con, &wrset);
  close (delme->con);
  if (delme->inbuffer)
    free (delme->inbuffer);
  if (delme->yettosend)
    free (delme->yettosend);
  if (delme->name)
    free (delme->name);
  /*  Mudplayer is freed by main.m */
  /* if (delme->mudplayer) */
  /*   [delme->mudplayer free]; */
  free (delme);
}

void
cat_text_file (struct conlist *t, char *fname)
{
  FILE *fp;
  if ((fp = fopen (fname, "r")))
  {
    char buf[1000];
    while (fgets (buf, 1000, fp))
      text_sock_write (t, buf);
    fclose (fp);
  }
}

/* Actually send queued data (at last) */
void
flush_write (struct conlist *t)
{
  long a;

  /* Refuse to write to a socket which is marked for deletion. */
  if (t->type == delete)
  {
    /* Dump data */
    shorten_queue (&(t->yettosend), &(t->sendlen), t->sendlen, &(t->sendmemsize));
    return;
  }

  unbreak_pipe ();
  a = write (t->con, t->yettosend, (unsigned) (t->sendlen));
  if (broke_pipe ())
  {
    delmark (t);
    return;
  }
  if (a < 0)
  {
    if (errno != EWOULDBLOCK && errno != EAGAIN)
    {
      fflush (stdout);
      perror ("write");
      fflush (stderr);
      delmark (t);
      return;
    }
    return;
  }
  if (a)
    shorten_queue (&(t->yettosend), &(t->sendlen), a, &(t->sendmemsize));

  /* Remove from wrset those connections which have been flushed.
   */
  if (t->type != delete)
    if ((!(t->sendlen)) && FD_ISSET (t->con, &wrset))
      FD_CLR (t->con, &wrset);

  /* Add to wrset those connections which have outstanding junk. */
  if ((t->sendlen) && (!(FD_ISSET (t->con, &wrset))))
    FD_SET (t->con, &wrset);
}

/* Find a player's connection */
struct conlist *
findcon (char *dude)
{
  struct conlist *a = cons;
  while (a)
  {
    if (strcmp (a->name, dude) == 0 && a->type == player)
      break;
    a = a->next;
  }
  return a;
}

void
service_cons ()
{
  int busy;
  struct conlist *t;

  /* This program performs internal buffering to break packets into separate
   * commands and to coalesce them into large data objects.  It is necessary to
   * loop here until all outstanding data have been read since data in our
   * internal buffers will not trigger the select.
   */

  do {
    /*  This is about as far down as I feel comfortable putting the heartbeat. */
    /*  This way, everybody gets at least one round-robin shot at typing */
    /*  something between heartbeats. */
    do_heartbeats ();
    busy = 0;
    accept_new_cons ();
    t = cons;
    while (t)
    {
      if (t->type == delete)
      {
        struct conlist *u = t->next;
        (*(t->notify)) (t);
        remove_con (t);
        t = u;
      }
      else
      {
        if (t->sendlen)
          flush_write (t);
        /* flush_write can delmark t */
        if (t->type != delete)
        {
          /* Try to read */
          switch (get_message (&messbuf, &messbufsize, t)) {
          case -1:
            /* Connection has gone amok -- close it */
            delmark (t);
            break;
          case 0:
            /* No traffic */
            break;
          case 1:
            /* Got a message */
            busy = 1;
            (*(t->handler)) (messbuf, t);
          }
        }
        t = t->next;
      }
    }
  } while (busy);
}

void
wait_for_activity ()
{
  int ts;
  struct timeval tv;
  tfdset = fdset; twrset = wrset;
  /* We want to try to keep up the heartbeats */
  /* NOTE:  There is a good reason that the minimum for tv is 0/1, not 0/0. */
  /* _Bad_ things happen when it's zero! */
  tv.tv_sec = 0;
  tv.tv_usec = min (1, (heartbeat_microseconds - timesincelastheartbeat ()));
  ts = select (nfds+1, &tfdset, &twrset, &tfdset, &tv);
  if (ts < 0)
  {
    fflush (stdout);
    perror ("select");
    fflush (stderr);
    /* DON'T PANIC!  Any old signal will cause this. */
  }
}

void
show_users (struct conlist *tty)
{
  char temp[80];
  int l;
  struct conlist *t = cons;
  text_sock_write (tty, "Cheezmud User   Level   Minutes Idle\n");
  text_sock_write (tty, "====================================\n");
  while (t) {
    switch (t->type) {
    case player:
      l = [(t->mudplayer) level];
      if (l == -1)
        sprintf (temp, "                Admin     %.2f\n",
          (float)(time(NULL) - t->lastrecv) / 60.0);
      else
        sprintf (temp, "                %5d     %.2f\n",
          l, (float)(time(NULL) - t->lastrecv) / 60.0);
      strncpy (temp, t->name, min (strlen (t->name), 13));
      text_sock_write (tty, temp);
      break;
    case new:
    case auth:
      sprintf (temp, "<login>                     %.2f\n",
        (float)(time(NULL) - t->lastrecv) / 60.0);
      text_sock_write (tty, temp);
      break;
    case delete:
      ;
    }
    t = t->next;
  }
}

void
de_notify (struct conlist *t)
{
  t->notify = no_notify;
}