/*
 * gopherjoker, part of the Observable Gopherspace Universe Project
 * Copyright (C) 2019 Mateusz Viste
 *
 * 2019-02-27: host is removed if its main menu has no reference to itself
 * 2019-02-20: fixed null ptr dereference, added a 'down' count to countfile
 * 2019-02-19: countfile contains 3 values: total, active and pending servers
 * 2019-02-18: first public release
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <errno.h>
#include <fcntl.h>
#include <netdb.h>   /* gethostbyname() */
#include <stdio.h>   /* printf(), fopen(), ... */
#include <stdlib.h>  /* calloc(), rand() */
#include <string.h>  /* strcpy() */
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>    /* ctime(), time_t */
#include <unistd.h>  /* sleep() */

#include "../cmods/csv.c"

struct gopherlist {
  time_t failedsince;
  struct gopherlist *next;
  struct gopherlist *prev;
  unsigned short port;
  char *selector;
  char fqdn[1];
};

#define WAITPERIOD 10
#define SAVEPERIOD 3600
#define NEWHOSTSLIST "/tmp/gjoker_newhosts.csv"
#define MAXFAILTIME (3600 * 24 * 60)
#define TTLINIT 64


/**************** FUNCTIONS ****************/


static void glist_free(struct gopherlist *glist) {
  struct gopherlist *victim;
  while (glist) {
    free(glist->selector);
    victim = glist;
    glist = glist->next;
    free(victim);
  }
}


/* returns pointer to dbfile name */
static int parseargs(int argc, char **argv, char **dbfile, char **dbcount) {
  int i;
  *dbfile = NULL;
  *dbcount = NULL;
  for (i = 1; i < argc; i++) {
    if ((argv[i][0] != '-') && ((*dbfile == NULL) || (*dbcount == NULL))) {
      if (*dbfile == NULL) {
        *dbfile = argv[i];
      } else {
        *dbcount = argv[i];
      }
    } else {
      return(-1);
    }
  }
  return(0);
}


static struct gopherlist *glist_findhost(struct gopherlist *glist, const char *host, const unsigned short port) {
  struct gopherlist *node;
  for (node = glist; node != NULL; node = node->next) {
    if ((node->port == port) && (strcasecmp(node->fqdn, host) == 0)) return(node);
  }
  return(NULL);
}


/* add new host to glist, unless said host:port pair already exists there.
 * returns pointer to the new (or already existing) struct. NULL on error. */
static struct gopherlist *glist_addnewhost(struct gopherlist **glist, unsigned long *glistlen, char *newhost, unsigned short newport, time_t failedsince) {
  struct gopherlist *node;

  /* is entry in list already? */
  node = glist_findhost(*glist, newhost, newport);
  if (node != NULL) return(node);

  /* add it */
  node = calloc(1, sizeof(struct gopherlist) + strlen(newhost));
  if (node == NULL) return(NULL);
  /* */
  strcpy(node->fqdn, newhost);
  node->port = newport;
  node->failedsince = failedsince;
  node->prev = NULL;
  node->next = *glist;
  if (*glist != NULL) (*glist)->prev = node;
  *glist = node;
  *glistlen += 1;
  return(*glist);
}


static struct gopherlist *loaddb(const char *fname, unsigned long *glistlen) {
  FILE *fd;
  int i;
  char *ptrs[4];
  char lbuf[256];
  struct gopherlist *res = NULL;
  *glistlen = 0;
  fd = fopen(fname, "rb");
  if (fd == NULL) return(NULL);
  for (;;) {
    i = csvreadline(lbuf, sizeof(lbuf), ptrs, 4, fd);
    if (i != 0) break;
    /* TSV line structure
     * 0  hostname
     * 1  port
     * 2  failedsince (time_t) */

    if (glist_addnewhost(&res, glistlen, ptrs[0], atoi(ptrs[1]), atol(ptrs[2])) == NULL) {
      /* on error, free list and quit */
      glist_free(res);
      *glistlen = 0;
      return(NULL);
    }
  }
  fclose(fd);
  return(res);
}


static unsigned long savedb(const char *fname, const char *fcount, struct gopherlist *glist) {
  unsigned long count = 0, countactive = 0, countpending = 0, countdown = 0;
  FILE *f;
  struct gopherlist *node;
  f = fopen(fname, "wb");
  if (f == NULL) return(0);
  for (node = glist; node != NULL; node = node->next) {
    count++;
    if (node->failedsince == 0) countactive++;
    if (node->failedsince == 1) countpending++;
    if (node->failedsince > 1) countdown++;
    fprintf(f, "%s,%u,%ld\n", node->fqdn, node->port, node->failedsince);
  }
  fclose(f);
  f = fopen(fcount, "wb");
  if (f == NULL) return(0);
  fprintf(f, "%lu,%lu,%lu,%lu\n", count, countactive, countpending, countdown);
  fclose(f);
  return(count);
}


static struct gopherlist *dropnode(struct gopherlist *glist, struct gopherlist *node, unsigned long *glistlen) {
  if (node->prev == NULL) {
    glist = node->next;
    glist->prev = NULL;
    *glistlen -= 1;
    return(glist);
  }
  node->prev->next = node->next;
  if (node->next != NULL) {
    node->next->prev = node->prev;
  }
  *glistlen -= 1;
  return(glist);
}


static struct gopherlist *loadextrahosts(struct gopherlist *glist, const char *fname, unsigned long *glistlen) {
  FILE *f;
  char *ptrs[2];
  char buff[64];
  f = fopen(fname, "rb");
  if (f == NULL) return(glist);

  while (csvreadline(buff, sizeof(buff), ptrs, 2, f) == 0) {
    int port = atoi(ptrs[1]);
    if ((port < 1) || (port > 0xffff)) port = 70;
    if (glist_addnewhost(&glist, glistlen, ptrs[0], port, 0) == NULL) break;
  }
  fclose(f);

  return(glist);
}


static struct gopherlist *pickrandhostfromlist(struct gopherlist *glist, unsigned long glistlen) {
  static long choice;
  long i;
  if (glistlen == 0) return(NULL);
  choice += rand();
  choice %= glistlen;
  for (i = 0; i < choice; i++) {
    if (glist == NULL) return(NULL);
    glist = glist->next;
  }
  return(glist);
}


static int connect_nonblocking(int s, const struct sockaddr *addr, socklen_t addrlen, int timeout) {
  int r;
  struct timeval t;
  fd_set selset;
  for (;;) {
    r = connect(s, addr, addrlen);
    if (r == 0) return(0);
    if (errno != EINPROGRESS) return(r);
    FD_ZERO(&selset);
    FD_SET(s, &selset);
    t.tv_sec = timeout;
    t.tv_usec = 0;
    r = select(s + 1, NULL, &selset, NULL, &t);
    if (r < 0) return(-3);
    r = connect(s, addr, addrlen);
    return(r);
  }
}


static long gopher_fetch(char *buff, unsigned short buffsz, const char *host, unsigned short port, const char *selector) {
  int sock;
  int len;
  int flags;
  time_t timeout;
  struct addrinfo *addr, *addrptr;

  /* resolve host & connect */
  if (getaddrinfo(host, NULL, NULL, &addr) != 0) return(-1);
  for (addrptr = addr; addrptr != NULL; addrptr = addrptr->ai_next) {
    struct sockaddr_in *sin;
    sock = socket(addrptr->ai_family, addrptr->ai_socktype, addrptr->ai_protocol);
    if (sock < 0) continue;
    /* set port */
    sin = (void *)(addrptr->ai_addr);
    sin->sin_port = htons(port);
    /* set socket as non-blocking */
    flags = fcntl(sock, F_GETFL);
    fcntl(sock, F_SETFL, flags | O_NONBLOCK);
    /* try to connect */
    if (connect_nonblocking(sock, addrptr->ai_addr, addrptr->ai_addrlen, 10) == 0) {
      break;
    } else { /* close sock and try next option (if any) */
      close(sock);
    }
  }
  freeaddrinfo(addr);
  if (addrptr == NULL) return(-2);

  /* send selector, terminated by a CR/LF pair */
  if (selector != NULL) send(sock, selector, strlen(selector), MSG_MORE);
  send(sock, "\r\n", 2, 0);

  /* fetch answer until end of transmission or timeout */
  len = 0;
  timeout = time(NULL) + 10;
  for (;;) {
    unsigned short spaceleft = buffsz - len;
    int rlen;
    if (time(NULL) > timeout) {
      len = -3;
      break;
    }
    if (spaceleft == 0) break;  /* I'm stuffed thank you */
    rlen = recv(sock, buff + len, spaceleft, 0);
    if (rlen == 0) break;       /* orderly shutdown */
    if (rlen < 0) {             /* sock error */
      if ((errno == EWOULDBLOCK) || (errno == EAGAIN)) continue;
      len = -3;
      break;
    }
    len += rlen;
  }

  /* close sock and quit */
  close(sock);
  return(len);
}


/* read a gopher line entry and fill host, port and selector accordingly.
 * returns 0 on success, non-zero otherwise. */
static int readgline(char *host, unsigned short hostsz, unsigned short *port, char *selector, unsigned short selectorsz, const char *menu, unsigned short menulen) {
  char portstr[8];
  unsigned short i, t;
  /* skip first tabs (description) */
  for (;;) {
    i++;
    /* printf("i=%d ['%c']\n", i, menu[i]); */
    if (menu[i] == '\t') break;
    if (menu[i] == '\r') return(-2);
    if (menu[i] == '\n') return(-3);
    if (i >= menulen) return(-4);
  }
  /* read selector */
  for (t = 0; ; t++) {
    if (t >= selectorsz) return(-5);
    i++;
    if (i >= menulen) return(-6);
    if (menu[i] == '\r') return(-7);
    if (menu[i] == '\n') return(-8);
    selector[t] = menu[i];
    if (selector[t] == '\t') {
      selector[t] = 0;
      break;
    }
  }
  /* read hostname */
  for (t = 0; ; t++) {
    if (t >= hostsz) return(-9);
    i++;
    if (i >= menulen) return(-10);
    if (menu[i] == '\r') return(-11);
    if (menu[i] == '\n') return(-12);
    host[t] = menu[i];
    if (host[t] == '\t') {
      host[t] = 0;
      break;
    }
  }
  /* read port */
  for (t = 0; ; t++) {
    if (t >= sizeof(portstr)) return(-13);
    i++;
    if (i >= menulen) return(-14);
    portstr[t] = menu[i];
    if ((portstr[t] == '\t') || (portstr[t] == '\n')) {
      int tport;
      portstr[t] = 0;
      tport = atoi(portstr);
      if ((tport < 1) || (tport > 0xffff)) return(-15);
      *port = tport;
      break;
    }
  }
  return(0);
}


static int ishostvalid(const char *host) {
  unsigned short i;
  for (i = 0; host[i] != 0; i++) {
    if ((host[i] >= 'a') && (host[i] <= 'z')) continue;
    if ((host[i] >= 'A') && (host[i] <= 'Z')) continue;
    if ((host[i] >= '0') && (host[i] <= '9')) continue;
    if (host[i] == '-') continue;
    if (host[i] == '.') continue;
    return(-1);
  }
  if (i < 3) return(-10); /* host len should be at least 3 chars long */
  return(0);
}


/* parses a gopher menu and remembers all the hosts presents in its links */
static struct gopherlist *menu2gopherlist(const char *menu, unsigned short menulen) {
  long i;
  struct gopherlist *res = NULL;
  struct gopherlist *node;

  /* DEBUG */
  printf("------------------------------------------------\n");
  for (i = 0; i < menulen; i++) printf("%c", menu[i]);
  printf("------------------------------------------------\n");

  /* iterate line by line */
  for (i = 0; i < menulen; i++) {
    if (menu[i] == '1') {
      char host[64];
      char selector[128];
      unsigned short port;

      if (readgline(host, sizeof(host), &port, selector, sizeof(selector),  menu + i, menulen - i) == 0) {
        /* validate host name */
        if (ishostvalid(host) != 0) continue;
        /* */
        node = calloc(1, sizeof(struct gopherlist) + strlen(host));
        if (node == NULL) {
          glist_free(res);
          printf("ERR: OUT OF MEMORY\n");
          return(NULL);
        }
        node->port = port;
        strcpy(node->fqdn, host);
        node->next = res;
        node->selector = strdup(selector);
        if (res != NULL) res->prev = node;
        res = node;
      }
    }
    /* skip to next line */
    for (;;) {
      i++;
      if (i >= menulen) break;
      if (menu[i] == '\n') break;
    }
  }

  return(res);
}


static void gnode_free(struct gopherlist **node) {
  if (*node == NULL) return;
  free((*node)->selector);
  free(*node);
  *node = NULL;
}


static struct gopherlist *gnodedup(struct gopherlist *node) {
  struct gopherlist *res;
  if (node == NULL) return(NULL);
  res = calloc(1, sizeof(struct gopherlist) + strlen(node->fqdn));
  if (res == NULL) return(NULL);

  memcpy(res, node, sizeof(struct gopherlist) + strlen(node->fqdn));
  if (node->selector != NULL) res->selector = strdup(node->selector);
  return(res);
}


/**************** MAIN ****************/

int main(int argc, char **argv) {
  time_t nextaction = 0;
  time_t nextdbsave = 0;
  struct gopherlist *glist, *mlist, *gnode;
  struct gopherlist *curhost = NULL;
  int ttl = 0;
  char buff[0xffff];
  long bufflen;
  char *dbfile, *dbfilecnt;
  unsigned long glistlen;
  int i;

  if (parseargs(argc, argv, &dbfile, &dbfilecnt) != 0) {
    printf("usage: gopherjoker dbfile.csv dbcount.txt\n");
    return(1);
  }

  /* load db file */
  glist = loaddb(dbfile, &glistlen);

  /* init random engine */
  srand(time(NULL));

  for (;;) {

    printf("\n\n\n\n\n");

    /* do not browse too fast */
    while (time(NULL) < nextaction) sleep(1);
    nextaction = time(NULL) + WAITPERIOD;

    /* if extra manual hosts required to be added, do it now */
    glist = loadextrahosts(glist, NEWHOSTSLIST, &glistlen);

    /* save the db once every hour */
    if (time(NULL) > nextdbsave) {
      unsigned long savedbcount;
      printf("dumping hosts lists to %s\n", dbfile);
      savedbcount = savedb(dbfile, dbfilecnt, glist);
      if (savedbcount != glistlen) {
        printf("ERR: savedbcount != glistlen @ %d (%lu != %lu)\n", __LINE__, savedbcount, glistlen);
      }
      nextdbsave = time(NULL) + SAVEPERIOD;
    }

    /* if ttl expired, pick a new host to browse */
    if ((--ttl == 0) || (curhost == NULL)) {
      printf("TTL expired -> picking new host\n");
      curhost = pickrandhostfromlist(glist, glistlen);
      curhost = gnodedup(curhost);
      ttl = TTLINIT;
    }

    printf("TTL=%d | glistlen=%ld\n", ttl, glistlen);

    /* if no host, continue */
    if (curhost == NULL) {
      printf("hosts list empty. add some through " NEWHOSTSLIST ".\n");
      continue;
    } else {
      printf("fetching %s:%u/1%s ...\n", curhost->fqdn, curhost->port, curhost->selector);
    }

    /* fetch selector */
    bufflen = gopher_fetch(buff, sizeof(buff), curhost->fqdn, curhost->port, curhost->selector);
    if (bufflen < 1) { /* fail */
      printf("failed\n");
      /* if it was about root selector, see if it's time to drop the server */
      if (curhost->selector == NULL) {
        gnode = glist_findhost(glist, curhost->fqdn, curhost->port);
        if (gnode->failedsince == 0) {
          /* server was working at some point in the past, but not anymore */
          gnode->failedsince = time(NULL);
          printf("server %s:%u went down -> flaged as failed since now (%s)\n", gnode->fqdn, gnode->port, ctime(&(gnode->failedsince)));
        } else if (time(NULL) > curhost->failedsince + MAXFAILTIME) {
          /* remove server from the list */
          printf("server removed due to long-time failure: %s:%u (failed since %s)\n", gnode->fqdn, gnode->port, ctime(&(gnode->failedsince)));
          glist = dropnode(glist, gnode, &glistlen);
          gnode_free(&gnode);
        }
      }
      gnode_free(&curhost);
      continue;
    }

    printf("ok (%ld bytes)\n", bufflen);

    /* menu to glist */
    mlist = menu2gopherlist(buff, bufflen);
    if (mlist == NULL) {
      printf("ERR: no entries found in menu\n");
      glist_free(mlist);
      gnode_free(&curhost);
      continue;
    }

    /* try adding hosts to global glist (and count 'em) */
    i = 0;
    for (gnode = mlist; gnode != NULL; gnode = gnode->next) {
      i++;
      glist_addnewhost(&glist, &glistlen, gnode->fqdn, gnode->port, 1);
    }

    /* if main menu, then check that host is pointing to himself */
    if (curhost->selector == NULL) {
      if (glist_findhost(mlist, curhost->fqdn, curhost->port) == NULL) {
        printf("ERR: main menu contains no link to self, dropping hostname '%s'\n", curhost->fqdn);
        gnode = glist_findhost(glist, curhost->fqdn, curhost->port);
        glist = dropnode(glist, gnode, &glistlen);
        glist_free(mlist);
        gnode_free(&curhost);
        continue;
      }
    }

    /* mark host as 'okay' */
    gnode = glist_findhost(glist, curhost->fqdn, curhost->port);
    gnode->failedsince = 0;

    /* choose a random entry from mlist */
    gnode = pickrandhostfromlist(mlist, i);
    if (gnode == NULL) {
      printf("ERR: pickrandhostfromlist() failed @ %d\n", __LINE__);
      gnode_free(&curhost);
      glist_free(mlist);
      continue;
    }
    curhost = gnodedup(gnode);

    glist_free(mlist);
  }

  return(0);
}
