/*
* Copyright 1999 Red Hat Software, Inc.
*
*  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.
*
*/

/* This probably doesn't work on anything other then Ethernet! It may work
  on PLIP as well, but ARCnet and Token Ring are unlikely at best. */

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <net/if_packet.h>
#include <net/route.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <popt.h>
#include <resolv.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <syslog.h>
#include <unistd.h>

#include "config.h"
#include "pump.h"

#define N_(foo) (foo)

#define PROGNAME "pump"
#define CONTROLSOCKET "/var/run/pump.sock"

#define _(foo) ((foo))
#include <stdarg.h>

struct command {
   enum { CMD_STARTIFACE, CMD_RESULT, CMD_DIE, CMD_STOPIFACE,
          CMD_FORCERENEW, CMD_REQSTATUS, CMD_STATUS } type;
   union {
       struct {
           char device[20];
           int flags;
           int reqLease;                       /* in hours */
           char reqHostname[200];
       } start;
       int result;                             /* 0 for success */
       struct {
           char device[20];
       } stop;
       struct {
           char device[20];
       } renew;
       struct {
           char device[20];
       } reqstatus;
       struct {
           struct pumpNetIntf intf;
           char hostname[1024];
           char domain[1024];
           char bootFile[1024];
       } status;
   } u;
};

static int openControlSocket(char * configFile);

char * readSearchPath(void) {
   int fd;
   struct stat sb;
   char * buf;
   char * start;

   fd = open("/etc/resolv.conf", O_RDONLY);
   if (fd < 0) return NULL;

   fstat(fd, &sb);
   buf = alloca(sb.st_size + 2);
   if (read(fd, buf, sb.st_size) != sb.st_size) return NULL;
   buf[sb.st_size] = '\n';
   buf[sb.st_size + 1] = '\0';
   close(fd);

   start = buf;
   while (start && *start) {
       while (isspace(*start) && (*start != '\n')) start++;
       if (*start == '\n') {
           start++;
           continue;
       }

       if (!strncmp("search", start, 6) && isspace(start[6])) {
           start += 6;
           while (isspace(*start) && *start != '\n') start++;
           if (*start == '\n') return NULL;

           buf = strchr(start, '\n');
           *buf = '\0';
           return strdup(start);
       }
   }

   return NULL;
}

static void createResolvConf(struct pumpNetIntf * intf, char * domain,
                            int isSearchPath) {
   FILE * f;
   int i;
   char * chptr;

   /* force a reread of /etc/resolv.conf if we need it again */
   res_close();

   if (!domain) {
       domain = readSearchPath();
       if (domain) {
           chptr = alloca(strlen(domain) + 1);
           strcpy(chptr, domain);
           free(domain);
           domain = chptr;
           isSearchPath = 1;
       }
   }

   f = fopen("/etc/resolv.conf", "w");
   if (!f) {
       syslog(LOG_ERR, "cannot create /etc/resolv.conf: %s\n",
              strerror(errno));
       return;
   }

   if (domain && isSearchPath) {
       fprintf(f, "search %s\n", domain);
   } else if (domain) {
       fprintf(f, "search");
       chptr = domain;
       do {
           fprintf(f, " %s", chptr);
           chptr = strchr(chptr, '.');
           if (chptr) {
               chptr++;
               if (!strchr(chptr, '.'))
                   chptr = NULL;
           }
       } while (chptr);

       fprintf(f, "\n");
   }

   for (i = 0; i < intf->numDns; i++)
       fprintf(f, "nameserver %s\n", inet_ntoa(intf->dnsServers[i]));

   fclose(f);

   /* force a reread of /etc/resolv.conf */
   endhostent();
}

void setupDns(struct pumpNetIntf * intf, struct pumpOverrideInfo * override) {
   char * hn, * dn = NULL;
   struct hostent * he;

   if (override->flags & OVERRIDE_FLAG_NODNS) {
       return;
   }

   if (override->searchPath) {
       createResolvConf(intf, override->searchPath, 1);
       return;
   }

   if (intf->set & PUMP_NETINFO_HAS_DNS) {
       if (!(intf->set & PUMP_NETINFO_HAS_DOMAIN))  {
           if (intf->set & PUMP_NETINFO_HAS_HOSTNAME) {
               hn = intf->hostname;
           } else {
               createResolvConf(intf, NULL, 0);

               he = gethostbyaddr((char *) &intf->ip, sizeof(intf->ip),
                                  AF_INET);
               if (he) {
                   hn = he->h_name;
               } else {
                   hn = NULL;
               }
           }

           if (hn) {
               dn = strchr(hn, '.');
               if (dn)
                   dn++;
           }
       } else {
           dn = intf->domain;
       }

       createResolvConf(intf, dn, 0);
   }
}

static void runDaemon(int sock, char * configFile) {
   int conn;
   struct sockaddr_un addr;
   int addrLength;
   struct command cmd;
   struct pumpNetIntf intf[20];
   int numInterfaces = 0;
   int i;
   int closest;
   struct timeval tv;
   fd_set fds;
   struct pumpOverrideInfo * overrides = NULL;
   struct pumpOverrideInfo emptyOverride, * o;

   readPumpConfig(configFile, &overrides);
   if (!overrides) {
       overrides = &emptyOverride;
       overrides->intf.device[0] = '\0';
   }

   while (1) {
       FD_ZERO(&fds);
       FD_SET(sock, &fds);

       tv.tv_sec = tv.tv_usec = 0;
       closest = -1;
       if (numInterfaces) {
           for (i = 0; i < numInterfaces; i++)
               if ((intf[i].set & PUMP_INTFINFO_HAS_LEASE) &&
                       (closest == -1 ||
                              (intf[closest].renewAt > intf[i].renewAt)))
                   closest = i;
           if (closest != -1) {
               tv.tv_sec = intf[closest].renewAt - time(NULL);
               if (tv.tv_sec <= 0) {
                   pumpDhcpRenew(intf + closest);
                   continue;
               }
           }
       }

       if (select(sock + 1, &fds, NULL, NULL,
                  closest != -1 ? &tv : NULL) > 0) {
           conn = accept(sock, &addr, &addrLength);

           if (read(conn, &cmd, sizeof(cmd)) != sizeof(cmd)) {
               close(conn);
               continue;
           }

           switch (cmd.type) {
             case CMD_DIE:
               for (i = 0; i < numInterfaces; i++)
                   pumpDhcpRelease(intf + i);

               syslog(LOG_INFO, "terminating at root's request");

               cmd.type = CMD_RESULT;
               cmd.u.result = 0;
               write(conn, &cmd, sizeof(cmd));
               exit(0);

             case CMD_STARTIFACE:
               o = overrides;
               while (*o->intf.device &&
                       strcmp(o->intf.device, cmd.u.start.device)) {
                   o++;
               }
               if (!*o->intf.device) o = overrides;

               if (pumpDhcpRun(cmd.u.start.device,
                               cmd.u.start.flags, cmd.u.start.reqLease,
                               cmd.u.start.reqHostname[0] ?
                                   cmd.u.start.reqHostname : NULL,
                               intf + numInterfaces, o)) {
                   cmd.u.result = 1;
               } else {
                   pumpSetupInterface(intf);

                   syslog(LOG_INFO, "configured interface %s", intf->device);

                   if (intf->set & PUMP_NETINFO_HAS_GATEWAY) {
                       pumpSetupDefaultGateway(&intf->gateway);
                   }

                   setupDns(intf, o);

                   cmd.u.result = 0;
                   numInterfaces++;
               }
               break;

             case CMD_FORCERENEW:
               for (i = 0; i < numInterfaces; i++)
                   if (!strcmp(intf[i].device, cmd.u.renew.device)) break;
               if (i == numInterfaces)
                   cmd.u.result = RESULT_UNKNOWNIFACE;
               else
                   cmd.u.result = pumpDhcpRenew(intf + i);
               break;

             case CMD_STOPIFACE:
               for (i = 0; i < numInterfaces; i++)
                   if (!strcmp(intf[i].device, cmd.u.stop.device)) break;
               if (i == numInterfaces)
                   cmd.u.result = RESULT_UNKNOWNIFACE;
               else {
                   cmd.u.result = pumpDhcpRelease(intf + i);
                   if (numInterfaces == 1) {
                       cmd.type = CMD_RESULT;
                       write(conn, &cmd, sizeof(cmd));

                       syslog(LOG_INFO, "terminating as there are no "
                               "more devices under management");

                       exit(0);
                   }

                   intf[i] = intf[numInterfaces - 1];
                   numInterfaces--;
               }
               break;

             case CMD_REQSTATUS:
               for (i = 0; i < numInterfaces; i++)
                   if (!strcmp(intf[i].device, cmd.u.stop.device)) break;
               if (i == numInterfaces) {
                   cmd.u.result = RESULT_UNKNOWNIFACE;
               } else {
                   cmd.type = CMD_STATUS;
                   cmd.u.status.intf = intf[i];
                   if (intf[i].set & PUMP_NETINFO_HAS_HOSTNAME)
                       strncpy(cmd.u.status.hostname,
                           intf->hostname, sizeof(cmd.u.status.hostname));
                   cmd.u.status.hostname[sizeof(cmd.u.status.hostname)] = '\0';

                   if (intf[i].set & PUMP_NETINFO_HAS_DOMAIN)
                       strncpy(cmd.u.status.domain,
                           intf->domain, sizeof(cmd.u.status.domain));
                   cmd.u.status.domain[sizeof(cmd.u.status.domain) - 1] = '\0';

                   if (intf[i].set & PUMP_INTFINFO_HAS_BOOTFILE)
                       strncpy(cmd.u.status.bootFile,
                           intf->bootFile, sizeof(cmd.u.status.bootFile));
                   cmd.u.status.bootFile[sizeof(cmd.u.status.bootFile) - 1] =
                                                                       '\0';
               }

             case CMD_STATUS:
             case CMD_RESULT:
               /* can't happen */
               break;
           }

           if (cmd.type != CMD_STATUS) cmd.type = CMD_RESULT;
           write(conn, &cmd, sizeof(cmd));

           close(conn);
       }
   }

   exit(0);
}

static int openControlSocket(char * configFile) {
   struct sockaddr_un addr;
   int sock;
   size_t addrLength;
   pid_t child;
   int status;

   if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
       return -1;

   addr.sun_family = AF_UNIX;
   strcpy(addr.sun_path, CONTROLSOCKET);
   addrLength = sizeof(addr.sun_family) + strlen(addr.sun_path);

   if (!connect(sock, (struct sockaddr *) &addr, addrLength))
       return sock;

   if (errno != ENOENT && errno != ECONNREFUSED) {
       fprintf(stderr, "failed to connect to %s: %s\n", CONTROLSOCKET,
               strerror(errno));
       close(sock);
       return -1;
   }

   if (!(child = fork())) {
       close(sock);

       close(0);
       close(1);
       close(2);

       if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
           syslog(LOG_ERR, "failed to create socket: %s\n", strerror(errno));
           exit(1);
       }

       unlink(CONTROLSOCKET);
       umask(077);
       if (bind(sock, (struct sockaddr *) &addr, addrLength)) {
           syslog(LOG_ERR, "bind to %s failed: %s\n", CONTROLSOCKET,
                   strerror(errno));
           exit(1);
       }
       umask(033);

       listen(sock, 5);

       if (fork()) _exit(0);

       openlog("pumpd", LOG_PID, LOG_DAEMON);
       {
           time_t t;

           t = time(NULL);
           syslog(LOG_INFO, "starting at %s\n", ctime(&t));
       }

       runDaemon(sock, configFile);
   }

   waitpid(child, &status, 0);
   if (!WIFEXITED(status) || WEXITSTATUS(status))
       return -1;

   if (!connect(sock, (struct sockaddr *) &addr, addrLength))
       return sock;

   fprintf(stderr, "failed to connect to %s: %s\n", CONTROLSOCKET,
           strerror(errno));

   return 0;
}

void printStatus(struct pumpNetIntf i, char * hostname, char * domain,
                char * bootFile) {
   int j;

   printf("Device %s\n", i.device);
   printf("\tIP: %s\n", inet_ntoa(i.ip));
   printf("\tNetmask: %s\n", inet_ntoa(i.netmask));
   printf("\tBroadcast: %s\n", inet_ntoa(i.broadcast));
   printf("\tNetwork: %s\n", inet_ntoa(i.network));
   printf("\tBoot server %s\n", inet_ntoa(i.bootServer));

   if (i.set & PUMP_NETINFO_HAS_GATEWAY)
       printf("\tGateway: %s\n", inet_ntoa(i.gateway));

   if (i.set & PUMP_INTFINFO_HAS_BOOTFILE)
       printf("\tBoot file: %s\n", bootFile);

   if (i.set & PUMP_NETINFO_HAS_HOSTNAME)
       printf("\tHostname: %s\n", hostname);

   if (i.set & PUMP_NETINFO_HAS_DOMAIN)
       printf("\tDomain: %s\n", domain);

   if (i.numDns) {
       printf("\tNameservers:");
       for (j = 0; j < i.numDns; j++)
           printf(" %s", inet_ntoa(i.dnsServers[j]));
       printf("\n");
   }

   if (i.set & PUMP_INTFINFO_HAS_LEASE) {
       printf("\tRenewal time: %s", ctime(&i.renewAt));
       printf("\tExpiration time: %s", ctime(&i.leaseExpiration));
   }
}

int main (int argc, char ** argv) {
   char * device = "eth0";
   char * hostname = "";
   poptContext optCon;
   int rc;
   int test = 0;
   int flags = 0;
   int lease = 6;
   int killDaemon = 0;
   int release = 0, renew = 0, status = 0, lookupHostname = 0;
   struct command cmd, response;
   char * configFile = "/etc/pump.conf";
   struct pumpOverrideInfo * overrides;
   int cont;
   struct poptOption options[] = {
           { "config-file", 'c', POPT_ARG_STRING, &configFile, 0,
                       N_("Configuration file to use instead of "
                          "/etc/pump.conf") },
           { "hostname", 'h', POPT_ARG_STRING, &hostname, 0,
                       N_("Hostname to request"), N_("hostname") },
           { "interface", 'i', POPT_ARG_STRING, &device, 0,
                       N_("Interface to configure (normally eth0)"),
                       N_("iface") },
           { "kill", 'k', POPT_ARG_NONE, &killDaemon, 0,
                       N_("Kill daemon (and disable all interfaces)"), NULL },
           { "lease", 'l', POPT_ARG_INT, &lease, 0,
                       N_("Lease time to request (in hours)"), N_("hours") },
           { "lookup-hostname", '\0', POPT_ARG_NONE, &lookupHostname, 0,
                       N_("Force lookup of hostname") },
           { "release", 'r', POPT_ARG_NONE, &release, 0,
                       N_("Release interface"), NULL },
           { "renew", 'R', POPT_ARG_NONE, &renew, 0,
                       N_("Force immediate lease renewal"), NULL },
           { "status", 's', POPT_ARG_NONE, &status, 0,
                       N_("Display interface status"), NULL },
           /*{ "test", 't', POPT_ARG_NONE, &test, 0,
                       N_("Don't change the interface configuration or "
                          "run as a deamon.") },*/
           POPT_AUTOHELP
           { NULL, '\0', 0, NULL, 0 }
       };

   optCon = poptGetContext(PROGNAME, argc, argv, options,0);
   poptReadDefaultConfig(optCon, 1);

   if ((rc = poptGetNextOpt(optCon)) < -1) {
       fprintf(stderr, _("%s: bad argument %s: %s\n"), PROGNAME,
               poptBadOption(optCon, POPT_BADOPTION_NOALIAS),
               poptStrerror(rc));
       return 1;
   }

   if (poptGetArg(optCon)) {
       fprintf(stderr, _("%s: no extra parameters are expected\n"), PROGNAME);
       return 1;
   }

   /* make sure the config file is parseable before going on any further */
   if (readPumpConfig(configFile, &overrides)) return 1;

   if (geteuid()) {
       fprintf(stderr, _("%s: must be run as root\n"), PROGNAME);
       exit(1);
   }

   if (test)
       flags = PUMP_FLAG_NODAEMON | PUMP_FLAG_NOCONFIG;
   if (lookupHostname)
       flags |= PUMP_FLAG_FORCEHNLOOKUP;

   cont = openControlSocket(configFile);
   if (cont < 0)
       exit(1);

   if (killDaemon) {
       cmd.type = CMD_DIE;
   } else if (status) {
       cmd.type = CMD_REQSTATUS;
       strcpy(cmd.u.reqstatus.device, device);
   } else if (renew) {
       cmd.type = CMD_FORCERENEW;
       strcpy(cmd.u.renew.device, device);
   } else if (release) {
       cmd.type = CMD_STOPIFACE;
       strcpy(cmd.u.stop.device, device);
   } else {
       cmd.type = CMD_STARTIFACE;
       strcpy(cmd.u.start.device, device);
       cmd.u.start.flags = flags;
       cmd.u.start.reqLease = lease * 60 * 60;
       strcpy(cmd.u.start.reqHostname, hostname);
   }

   write(cont, &cmd, sizeof(cmd));
   read(cont, &response, sizeof(response));

   if (response.type == CMD_RESULT && response.u.result &&
           cmd.type == CMD_STARTIFACE) {
       cont = openControlSocket(configFile);
       if (cont < 0)
           exit(1);
       write(cont, &cmd, sizeof(cmd));
       read(cont, &response, sizeof(response));
   }

   if (response.type == CMD_RESULT) {
       if (response.u.result) {
           fprintf(stderr, "Operation failed.\n");
           return 1;
       }
   } else if (response.type == CMD_STATUS) {
       printStatus(response.u.status.intf, response.u.status.hostname,
                   response.u.status.domain, response.u.status.bootFile);
   }

   return 0;
}