/*
 * this file is part of ethflopd, it provides the ethernet backend for Linux
 * Copyright (C) 2019-2024 Mateusz Viste
 */

#include <arpa/inet.h>       /* htons() */
#include <errno.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <signal.h>
#include <stdio.h>           /* stderr, fprintf()... */
#include <stdlib.h>          /* NULL etc */
#include <string.h>          /* memset(), memcpy()... */
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h>          /* unix socket */
#include <syslog.h>
#include <time.h>            /* time() */
#include <unistd.h>          /* close() */

#include "core.h"            /* ethflop_process() */
#include "version.h"         /* program version */


/* UDP port for control queries */
#define UDP_PORT 1390

static int datasock = -1;
static int ctrlsock = -1;
static int udpsock = -1;


/* the flag is set when ethflopd is expected to terminate */
static sig_atomic_t volatile terminationflag = 0;


static void sigcatcher(int sig) {
  switch (sig) {
    case SIGTERM:
    case SIGQUIT:
    case SIGINT:
      terminationflag = 1;
      break;
    default:
      break;
  }
}


/* opens the UDP control socket (for API queries, UDP/1390) */
static int open_udp_sock(void) {
  int sock;
  struct sockaddr_in sin;

  sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (sock == -1) {
    syslog(LOG_ERR, "ERROR: failed to init UDP socket (%s)", strerror(errno));
    fprintf(stderr, "ERROR: failed to init UDP socket (%s)\n", strerror(errno));
    return(-1);
  }

  /* prep sock struct */
  memset(&sin, 0, sizeof(sin));
  sin.sin_family = AF_INET;
  sin.sin_port = htons(UDP_PORT);
  sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

  /* bind */
  if (bind(sock, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
    syslog(LOG_ERR, "ERROR: failed to bind UDP/%u socket (%s)", UDP_PORT, strerror(errno));
    fprintf(stderr, "ERROR: failed to bind UDP/%u socket (%s)\n", UDP_PORT, strerror(errno));
    close(sock);
    return(-1);
  }

  return(sock);
}


static int raw_sock(const int protocol, const char *const interface, void *const hwaddr) {
  struct ifreq iface;
  struct sockaddr_ll addr;
  int socketfd, result;
  int ifindex;

  if ((interface == NULL) || (*interface == 0)) {
    errno = EINVAL;
    return(-1);
  }

  socketfd = socket(AF_PACKET, SOCK_RAW, htons(protocol));
  if (socketfd == -1) return(-1);

  do {
    memset(&iface, 0, sizeof iface);
    strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
    result = ioctl(socketfd, SIOCGIFINDEX, &iface);
    if (result == -1) break;
    ifindex = iface.ifr_ifindex;

    memset(&iface, 0, sizeof(iface));
    strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
    result = ioctl(socketfd, SIOCGIFFLAGS, &iface);
    if (result == -1) break;
    iface.ifr_flags |= IFF_PROMISC;
    result = ioctl(socketfd, SIOCSIFFLAGS, &iface);
    if (result == -1) break;

    memset(&iface, 0, sizeof iface);
    strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
    result = ioctl(socketfd, SIOCGIFHWADDR, &iface);
    if (result == -1) break;

    memset(&addr, 0, sizeof addr);
    addr.sll_family = AF_PACKET;
    addr.sll_protocol = htons(protocol);
    addr.sll_ifindex = ifindex;
    addr.sll_hatype = 0;
    addr.sll_pkttype = PACKET_HOST | PACKET_BROADCAST;
    addr.sll_halen = ETH_ALEN; /* Assume ethernet! */
    memcpy(&addr.sll_addr, &iface.ifr_hwaddr.sa_data, addr.sll_halen);
    if (hwaddr != NULL) memcpy(hwaddr, &iface.ifr_hwaddr.sa_data, ETH_ALEN);

    if (bind(socketfd, (struct sockaddr *)&addr, sizeof addr)) break;

    errno = 0;
    return(socketfd);
  } while (0);

  {
    const int saved_errno = errno;
    close(socketfd);
    errno = saved_errno;
    return(-1);
  }
}


static void eth_close(void) {
  if (datasock != -1) close(datasock);
  if (ctrlsock != -1) close(ctrlsock);
  if (udpsock != -1) close(udpsock);
}


static int eth_init(unsigned char *mymac, const char *ifname) {

  datasock = raw_sock(0xEFDD, ifname, mymac);
  if (datasock == -1) {
    syslog(LOG_ERR, "Error: failed to open socket (%s)", strerror(errno));
    fprintf(stderr, "Error: failed to open socket (%s)\n"
                    "\n"
                    "Usually ethflopd requires to be launched as root for\n"
                    "handling raw (ethernet) sockets. Are you root?\n", strerror(errno));
    eth_close();
    return(-1);
  }

  ctrlsock = raw_sock(0xEFDC, ifname, mymac);
  if (ctrlsock == -1) {
    syslog(LOG_ERR, "Error: failed to open socket (%s)", strerror(errno));
    fprintf(stderr, "Error: failed to open socket (%s)\n"
                    "\n"
                    "Usually ethflopd requires to be launched as root for\n"
                    "handling raw (ethernet) sockets. Are you root?\n", strerror(errno));
    eth_close();
    return(-1);
  }

  return(0);
}


static unsigned short eth_recv(void *frame, size_t framesz, struct sockaddr_in **udpclient) {
  static struct sockaddr_in sin;
  socklen_t sinlen = sizeof(struct sockaddr_in);
  fd_set fdset;
  int highestfd;
  int len;
  struct timeval stimeout = {2, 0}; /* set timeout to 2s */

  highestfd = datasock;
  if (ctrlsock > highestfd) highestfd = ctrlsock;
  if (udpsock > highestfd) highestfd = udpsock;

  /* prepare the set of descriptors to be monitored later through select() */
  FD_ZERO(&fdset);
  FD_SET(datasock, &fdset);
  FD_SET(ctrlsock, &fdset);
  FD_SET(udpsock, &fdset);

  *udpclient = NULL;

  /* wait for something to happen on my socket */
  if (select(highestfd + 1, &fdset, NULL, NULL, &stimeout) < 1) {
    return(0);
  }

  if (FD_ISSET(datasock, &fdset)) {
    len = recv(datasock, frame, framesz, MSG_DONTWAIT);
    if (len > 0) return((unsigned short)len);
  }

  if (FD_ISSET(ctrlsock, &fdset)) {
    len = recv(ctrlsock, frame, framesz, MSG_DONTWAIT);
    if (len > 0) return((unsigned short)len);
  }

  if (FD_ISSET(udpsock, &fdset)) {
    len = recvfrom(udpsock, frame, framesz, 0, (struct sockaddr *)&sin, &sinlen);
    if (len > 0) {
      *udpclient = &sin;
      return((unsigned short)len);
    }
  }

  return(0);
}


static int eth_send(const void *frame, unsigned short framelen) {
  int len;
  len = (int)send(datasock, frame, framelen, 0);
  if (len < 0) {
    syslog(LOG_ERR, "ERROR: send() returned %d (%s)", len, strerror(errno));
  } else if (len != framelen) {
    syslog(LOG_ERR, "ERROR: send() sent less than expected (%d != %lu)", len, sizeof(frame));
  } else {
    return(0);
  }

  return(-1);
}


/* daemonize the process, return 0 on success, non-zero otherwise */
static int daemonize(void) {
  pid_t mypid;

  /* I don't want to get notified about SIGHUP */
  signal(SIGHUP, SIG_IGN);

  /* fork off */
  mypid = fork();
  if (mypid == 0) { /* I'm the child, do nothing */
    /* nothing to do - just continue */
  } else if (mypid > 0) { /* I'm the parent - quit now */
    exit(0);
  } else {  /* error condition */
    return(-2);
  }
  return(0);
}


static void help(void) {
  puts("ethflopd version " PVER " | Copyright (C) " PDATE " Mateusz Viste\n"
         "http://ethflop.sourceforge.net\n"
         "\n"
         "usage: ethflopd [options] interface storagedir\n"
         "       ethflopd -c control query\n"
         "\n"
         "Options:\n"
         "  -f        Keep in foreground (do not daemonize)\n"
         "  -h        Display this information\n"
         "\n"
         "The '-c' mode is for sending control commands to ethflopd on behalf of\n"
         "an ethflop client. Possible queries:\n"
         "\n"
         "imglist       returns the list of available floppy images\n"
         "clilist       returns the list of clients\n"
         "e MAC         eject floppy used by client MAC (format 00:11:22:33:44:55:66)\n"
         "i MAC DNAME   insert floppy image DNAME to client MAC\n"
         "I MAC DNAME   same as 'i' but inserts the floppy img write-protected\n"
  );
}


static int udpclient(int argcount, char * const *args) {
  struct sockaddr_in addr;
  int fd;
  int i;
  char buff[1024];

  if (argcount == 0) {
    fprintf(stderr, "bad syntax, go read 'ethflopd --help' again.\n");
    return(-1);
  }

  fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (fd == -1) {
    fprintf(stderr, "ERROR: failed to open UDP socket (%s)\n", strerror(errno));
    return(-1);
  }

  /* prep sock struct */
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(UDP_PORT);
  addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

  buff[0] = 0;
  for (i = 0; i < argcount; i++) {
    if (i > 0) strcat(buff, " ");
    if (strlen(buff) + strlen(args[i]) + 2 > sizeof(buff)) {
      fprintf(stderr, "commandline too long\n");
      close(fd);
      return(-1);
    }
    strcat(buff, args[i]);
  }

  if (sendto(fd, buff, strlen(buff), MSG_NOSIGNAL, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
    fprintf(stderr, "ERROR: send() to control sock failed (%s)\n", strerror(errno));
    close(fd);
    return(-1);
  }

  /* wait for an answer */
  for (i = 0; i < 2; i++) {
    int len;
    fd_set set;
    struct timeval stimeout = {1, 0}; /* set timeout to 1s */

    /* prepare the set of descriptors to be monitored later through select() */
    FD_ZERO(&set);
    FD_SET(fd, &set);

    /* wait for something to happen on my socket */
    if (select(fd + 1, &set, NULL, NULL, &stimeout) < 1) continue;

    len = recv(fd, buff, sizeof(buff), 0);
    for (i = 0; i < len; i++) {
      if (buff[i] == '\r') continue;
      if (buff[i] == '$') continue;
      printf("%c", buff[i]);
    }
    puts("");
    break;
  }

  close(fd);

  /* timeout? */
  if (i == 2) {
    fprintf(stderr, "timeout\n");
    return(-1);
  }

  return(0);
}


/* converts a MAC string "AA:BB:CC:DD:EE:FF" into binary (6 bytes), returns
 * zero on success, non-zero on MAC parsing error */
static int macstr2bin(unsigned char *bin, const char *s) {
  int i, digcount;

  for (i = 0; i < 16;) {
    digcount = 0;
    NEXTDIGIT:
    *bin <<= 4;
    if ((s[i] >= '0') && (s[i] <= '9')) {
      *bin |= (s[i] - '0');
    } else if ((s[i] >= 'A') && (s[i] <= 'F')) {
      *bin |= (s[i] + 10 - 'A');
    } else if ((s[i] >= 'a') && (s[i] <= 'f')) {
      *bin |= (s[i] + 10 - 'a');
    } else { /* bad format (not hex) */
      return(-1);
    }
    i++;
    if (digcount++ == 0) goto NEXTDIGIT;
    i++; /* skip colon (:) */
    bin++;
  }

  return(0);
}


static struct cliententry *getclient_by_mac(const unsigned char *mac) {
  struct cliententry *clist = ethflop_getclients();
  while (clist != NULL) {
    if (memcmp(clist->mac, mac, 6) == 0) return(clist);
    clist = clist->next;
  }
  return(NULL);
}


/* processes an API (UDP) command of len bytes in buff. writes the answer to buff, up to buffsz bytes. returns the length of the returned answer */
static unsigned short process_udp_query(char *buff, unsigned short len, size_t buffsz) {
  /* terminate buff with a NUL for easier parsing */
  buff[len] = 0;

  /* list images (imglist) */
  if (strcasecmp(buff, "imglist") == 0) {
    unsigned short l = 0;
    char list[1024];
    unsigned short imgcount, i;
    imgcount = ethflop_imgfilelist(list, sizeof(list));

    l += sprintf(buff+l, "{\"images\": [\n");

    for (i = 0; i < imgcount; i++) {
      if ((buffsz - l) < 20) break;
      if (i > 0) {
        l += sprintf(buff+l, ",");
        if ((i & 7) == 0) l += sprintf(buff+l, "\n");
      }
      l += sprintf(buff+l, " \"%s\"", list + (i * 10));
    }

    l += sprintf(buff+l, "\n]}");
    return(l);
  }

  /* list clients (clilist) */
  if (strcasecmp(buff, "clilist") == 0) {
    unsigned short l;
    struct cliententry *clist = ethflop_getclients();

    l = sprintf(buff, "{ \"clients\": [");
    while (clist != NULL) {
      if ((buffsz - l) < 128) break;

      if (l > 20) l += sprintf(buff+l, ",");

      /* mac */
      l += sprintf(buff+l, "\n{\"mac\": \"%02X:%02X:%02X:%02X:%02X:%02X\", ", clist->mac[0], clist->mac[1], clist->mac[2], clist->mac[3], clist->mac[4], clist->mac[5]);

      /* img, roflag and sectcount */
      l += sprintf(buff+l, "\"img\": \"%s\", \"ro\": %u, \"sectcount\": %u}", clist->curflopid, clist->ro, clist->sectcount);

      clist = clist->next;
    }

    l += sprintf(buff+l, "\n]}");
    return(l);
  }

  /* eject */
  if ((buff[0] == 'e') && (buff[1] == ' ')) {
    unsigned char mac[6];
    char msg[256];
    struct cliententry *ce;

    if (macstr2bin(mac, buff + 2) != 0) {
      return(sprintf(buff, "{\"errmsg\": \"bad MAC\"}"));
    }

    ce = getclient_by_mac(mac);
    if (ce == NULL) return(sprintf(buff, "{\"errmsg\": \"client not found\"}"));

    if (ethflop_eject(ce, msg) == 0) {
      return(sprintf(buff, "{\"msg\": \"%s\"}", msg));
    } else {
      return(sprintf(buff, "{\"errmsg\": \"%s\"}", msg));
    }
    return(strlen(buff));
  }

  /* insert */
  if (((buff[0] == 'i') && (buff[1] == ' ')) ||
      ((buff[0] == 'I') && (buff[1] == ' '))) {
    unsigned char mac[6];
    int i;
    struct cliententry *ce;
    unsigned char roflag = 0;
    char msg[256];

    if (buff[0] == 'I') roflag = 1;

    if (macstr2bin(mac, buff + 2) != 0) {
      return(sprintf(buff, "{\"errmsg\": \"bad MAC\"}"));
    }

    /* skip MAC to find next argument */
    for (i = 2; ; i++) {
      if (buff[i] == ' ') break;
      if (buff[i] == 0) return(sprintf(buff, "{\"errmsg\": \"missing disk name argument\"}"));
    }
    /* skip all spaces */
    for (;; i++) {
      if (buff[i] == 0) return(sprintf(buff, "{\"errmsg\": \"missing disk name argument\"}"));
      if (buff[i] != ' ') break;
    }

    ce = getclient_by_mac(mac);
    if (ce == NULL) return(sprintf(buff, "{\"errmsg\": \"client not found\"}"));

    if (ethflop_insert(ce, buff + i, msg, roflag) == 0) {
      return(sprintf(buff, "{\"msg\": \"%s\"}", msg));
    } else {
      return(sprintf(buff, "{\"errmsg\": \"%s\"}", msg));
    }
  }

  /* else it is an unrecognized command */
  return(sprintf(buff, "{\"errmsg\": \"unrecognized command\"}"));
}


int main(int argc, char **argv) {
  unsigned char mymac[6];
  unsigned char frame[1024];
  char logmsg[128];
  const char *intname;
  const char *storagedir;
  int opt;
  int daemon = 1; /* daemonize self by default */
  struct timespec tp1, tp2; /* used for calculating response time */

  logmsg[0] = 0;

  /* client mode is a very special case */
  if ((argc > 1) && (strcmp(argv[1], "-c") == 0)) {
    return(udpclient(argc - 2, argv + 2));
  }

  while ((opt = getopt(argc, argv, "fh")) != -1) {
    switch (opt) {
      case 'f': /* -f: no daemon */
        daemon = 0;
        break;
      case 'h': /* -h: help */
        help();
        return(0);
      case '?': /* error */
        help();
        return(1);
    }
  }

  /* I expect exactly two positional arguments */
  if ((argc - optind) != 2) {
    help();
    return(1);
  }
  intname = argv[optind++];
  storagedir = argv[optind++];

  /* setup signals catcher */
  signal(SIGTERM, sigcatcher);
  signal(SIGQUIT, sigcatcher);
  signal(SIGINT, sigcatcher);

  /* switch to storage dir (that's where *.IMG files are) */
  if (chdir(storagedir) != 0) {
    fprintf(stderr, "chdir() to '%s' failed\n", storagedir);
    syslog(LOG_ERR, "chdir() to '%s' failed", storagedir);
    return(1);
  }

  udpsock = open_udp_sock();
  if (udpsock == -1) return(1);

  if (eth_init(mymac, intname) != 0) {
    fprintf(stderr, "eth_init() failed\n");
    syslog(LOG_ERR, "eth_init() failed");
    return(1);
  }

  if (daemon != 0) {
    if (daemonize() != 0) {
      fprintf(stderr, "Error: failed to daemonize!\n");
      syslog(LOG_ERR, "Error: failed to daemonize!");
      eth_close();
      return(1);
    }
  }

  puts("ethflopd ver " PVER " started");
  syslog(LOG_NOTICE, "ethflopd ver " PVER " started");

  printf("Listening on '%s' [%02X:%02X:%02X:%02X:%02X:%02X] ; storage dir=%s ; ctrl socket at UDP/%u\n", intname, mymac[0], mymac[1], mymac[2], mymac[3], mymac[4], mymac[5], storagedir, UDP_PORT);

  ethflop_init(mymac);

  /* main loop */
  while (terminationflag == 0) {
    const struct FRAME *answer;
    unsigned short framelen;
    struct sockaddr_in *udpclient;

    /* any log in buffer? output it to syslog and reset the buffer */
    if (logmsg[0] != 0) {
      syslog(LOG_NOTICE, "%s", logmsg);
      logmsg[0] = 0;
    }

    framelen = eth_recv(&frame, sizeof(frame), &udpclient);
    if (framelen == 0) continue;

    /* is this from the UDP socket? */
    if (udpclient != NULL) {

      framelen = process_udp_query((char *)(&frame), framelen, sizeof(frame));

      if (sendto(udpsock, frame, framelen, 0, (struct sockaddr *)udpclient, sizeof(struct sockaddr_in)) == -1) {
        syslog(LOG_WARNING, "sendto() failed on UDP sock (%s)", strerror(errno));
        fprintf(stderr, "sendto() failed on UDP sock (%s)\n", strerror(errno));
      }
      continue;
    }

    clock_gettime(CLOCK_MONOTONIC, &tp1); /* get cur time for later calculation */

    answer = ethflop_process(&frame, framelen, logmsg);
    if (answer == NULL) continue;

    /* wait 0.5 ms - just to make sure that ethflop had time to prepare itself - due to ethflop using a single buffer for send/recv operations it needs a tiny bit of time to switch from 'sending' to 'receiving'. 0.5 ms should be enough even for the slowest PC (4MHz) and the crappiest packet driver (that would need 2000 cycles to return from sending a packet) */
    {
      struct timespec nanot;
      nanot.tv_sec = 0;
      nanot.tv_nsec = 500 * 1000; /* 1000 ns is 1 us. 1000 us is 1 ms */
      nanosleep(&nanot, NULL);
    }

    if (eth_send(answer, sizeof(struct FRAME)) != 0) {
      syslog(LOG_ERR, "ERROR: send() failed");
    }

    clock_gettime(CLOCK_MONOTONIC, &tp2); /* get cur time */

    { /* compute answer time */
      long msec_diff;
      msec_diff = (tp2.tv_sec - tp1.tv_sec) * 1000;
      msec_diff += tp2.tv_nsec / 1000000l;
      msec_diff -= tp1.tv_nsec / 1000000l;
      if (msec_diff > 10) syslog(LOG_WARNING, "WARNING: query %04X took a long time to process (%ld ms) -> [%ld.%ld .. %ld.%ld]", ((unsigned short *)frame)[7], msec_diff, tp1.tv_sec, tp1.tv_nsec, tp2.tv_sec, tp2.tv_nsec);
    }
  }

  /* clean up and quit */
  eth_close();

  return(0);
}
