/* zlogger -- monolithic zephyr logger
 *
 * James M. Kretchmar
 * 1999
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/param.h>
#include <syslog.h>
#include <netdb.h>
#include <signal.h>
#include <regex.h>
#include <krb.h>
#include <zephyr/zephyr.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <com_err.h>
#include <string.h>
#include <netinet/in.h>

#define LINE 1024
#define REGOPTS REG_EXTENDED|REG_NOSUB|REG_ICASE

typedef struct struct_entry {
  char *class;
  regex_t instance;
  regex_t opcode;
  int negateinstance;
  int negateopcode;
  char *logfile;
  int logfilenoparse;
  int nolog;
  int logroll;
  int loglife;
  struct struct_entry *next;
} entry;

typedef struct struct_globalconfig {
  entry *entries;
  char *ticketfile;
  char *srvtab;
  char *kservice;
  int newtickets; /* in hours   */
  int newsubs;    /* in minutes */
  char hostname[MAXHOSTNAMELEN];
  char *cells;
} globalconfig;

int readconfig(char *filename, globalconfig *g);
char **atokenize(char *buffer, char *sep, int *i); 
void argserr(char *key, int line);
void keyerr(char *key, int line);
void regexerr(char *reg, int line);
char *mystrdup(char *foo);
void downstr(char *foo);
void get_tickets_and_tokens(globalconfig *g);
int ksrvtgt(char *principal, char *instance, char *in_srvtab, int timeunits);
void getsubs(globalconfig *g);
void lognotice(globalconfig *g, ZNotice_t *notice);
void writelog(entry *ent, ZNotice_t *notice);
int is_weird (char *instance);
void turnoverlogs(globalconfig *g);
void initlogs(globalconfig *g);
void securefile(char *path);

int main(int argc, char **argv) {
  globalconfig g;
  char *configfile = NULL;
  int c, i;
  char *p;
  extern char *optarg;
  extern int optind;
  int errflg = 0;
  int fflg = 0;
  int ret;
  char envbuff[LINE];
  time_t tixtime, substime;
  int noroll;
  
  while ((c = getopt(argc, argv, "fc:")) != EOF)
    switch (c) {
    case 'c':
      configfile=optarg;
      break;
    case 'f':
      fflg++;
      break;
    case '?':
      errflg++;
    }
  
  if (errflg || (configfile == NULL)) {
    fprintf(stderr, "Usage: %s [-f] -c <filename>\n", argv[0]);
    exit(1);
  }

  ret=readconfig(configfile, &g);
  if (ret) {
    fprintf(stderr, "Could not open configfile for reading\n");
    exit(1);
  }

  if (!fflg) {
    for(i=0; i<getdtablesize(); i++) {
      close(i);
    }

    ret=fork();
    if (ret == -1) {
      fprintf(stderr, "couldn't fork, going to die\n");
      exit(2);
    } else if (ret != 0) {
      /* parent exits */
      exit(0);
    }

    setsid();
    chdir("/");
    umask(0);
  }

  openlog("zlogger", LOG_PID, LOG_LOCAL0);

  snprintf(envbuff, LINE, "KRBTKFILE=%s", g.ticketfile);
  putenv(envbuff);

  setpag();

  if (gethostname(&(g.hostname), MAXHOSTNAMELEN)<0){
    syslog(LOG_ERR, "gethostname: %m");
    exit(1);
  }
  downstr(g.hostname);
  p=strchr(g.hostname, '.');
  if (p!=NULL) *p=0;


  get_tickets_and_tokens(&g);

  if ((ret = ZInitialize()) != ZERR_NONE) {
    com_err(argv[0],ret,"while initializing");
    exit(1);
  }
  if ((ret = ZOpenPort(NULL)) != ZERR_NONE) {
    com_err(argv[0],ret,"while opening port");
    exit(1);
  }

  getsubs(&g);

  initlogs(&g);
  
  tixtime = substime = time(NULL);
  noroll = 0;
  while (1) {
    time_t now;
    struct tm *nowdate;
    
    sleep(2);
    now=time(NULL);

    if (now-tixtime > (g.newtickets * 60 * 60)) {
      get_tickets_and_tokens(&g);
      tixtime=time(NULL);
    }

    if (now-substime > (g.newsubs * 60)) {
      getsubs(&g);
      substime=time(NULL);
    }

    nowdate=localtime(&now);
    if (!noroll && (nowdate->tm_hour == 3)) {
      noroll = 1;
      turnoverlogs(&g);
    } else if (nowdate->tm_hour == 4) {
      noroll = 0;
    }

    while (ZPending()) {
      static ZNotice_t notice;
      struct sockaddr_in from;
      ZReceiveNotice(&notice, &from);
      lognotice(&g, &notice);
      ZFreeNotice(&notice);
    }
    
  }

  return(0);
}


int readconfig(char *filename, globalconfig *g) {
  FILE *f;
  char line[LINE];
  int linecounter = 0;
  int i, k, ret;
  char **arg;
  entry *newentry, *tmpentry;

  f = fopen(filename, "r");
  if (!f) {
    perror(filename);
    return(-1);
  }

  memset(g, 0, sizeof(globalconfig));
  newentry = tmpentry = NULL;

  while(fgets(line, sizeof(line), f)!=NULL) {
    linecounter++;

    arg=(char **)atokenize(line, " \t\n", &i);
    if (i==0) continue;
    if (arg[0][0] == '#') continue;

    i--; /* so i is the number of args _after_ the keyword arg[0] */

    if (!strcasecmp(arg[0], "ticketfile")) {
      if (i!=1) argserr(arg[0], linecounter);
      g->ticketfile=mystrdup(arg[1]);
    } else if (!strcasecmp(arg[0], "srvtab")) {
      if (i!=1) argserr(arg[0], linecounter);
      g->srvtab=mystrdup(arg[1]);
    } else if (!strcasecmp(arg[0], "kservice")) {
      if (i!=1) argserr(arg[0], linecounter);
      g->kservice=mystrdup(arg[1]);
    } else if (!strcasecmp(arg[0], "newtickets")) {
      if (i!=1) argserr(arg[0], linecounter);
      g->newtickets=atoi(arg[1]);
    } else if (!strcasecmp(arg[0], "newsubs")) {
      if (i!=1) argserr(arg[0], linecounter);
      g->newsubs=atoi(arg[1]);
    } else if (!strcasecmp(arg[0], "cells")) {
      if (i<1) argserr(arg[0], linecounter);
      g->cells=malloc(50*i);
      strcpy(g->cells, "");
      for (k=0; k<i; k++) {
	strcat(g->cells, arg[k+1]);
	strcat(g->cells, " ");
      }
    } else if (!strcasecmp(arg[0], "define")) {
      /* create a new one */
      newentry=malloc(sizeof(entry));
      memset(newentry, 0, sizeof(entry));

      /* append it */
      if (g->entries == NULL) {
	g->entries = newentry;
      } else {
	tmpentry = g->entries;
	while(tmpentry->next != NULL) {
          tmpentry=tmpentry->next;
        }
        tmpentry->next=newentry;
      }
    } else if (!strcasecmp(arg[0], "class")) {
      if (i!=1) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      newentry->class=mystrdup(arg[1]);
    } else if (!strcasecmp(arg[0], "instance")) {
      if (i!=1) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      if (arg[1][0] != '!') {
	ret=regcomp(&(newentry->instance), arg[1], REGOPTS);
      } else {
	ret=regcomp(&(newentry->instance), arg[1]+1, REGOPTS);
	newentry->negateinstance = 1;
      }
      if (ret) regexerr(arg[1], linecounter);
    } else if (!strcasecmp(arg[0], "opcode")) {
      if (i!=1) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      if (arg[1][0] != '!') {
	ret=regcomp(&(newentry->opcode), arg[1], REGOPTS);
      } else {
	ret=regcomp(&(newentry->opcode), arg[1]+1, REGOPTS);
	newentry->negateopcode = 1;
      }
      if (ret) regexerr(arg[1], linecounter);
    } else if (!strcasecmp(arg[0], "logfile")) {
      if (i!=1) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      newentry->logfile=mystrdup(arg[1]);
      if (!strstr(arg[1], "%i")) newentry->logfilenoparse = 1;
    } else if (!strcasecmp(arg[0], "nolog")) {
      if (i!=1) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      if (!strcasecmp(arg[1], "yes")) {
	newentry->nolog = 1;
      } else {
	newentry->nolog = 0;
      }
    } else if (!strcasecmp(arg[0], "logroll")) {
      if (i!=2) argserr(arg[0], linecounter);
      if (newentry == NULL) keyerr(arg[0], linecounter);
      if (!strcasecmp(arg[1], "yes")) {
	newentry->logroll = 1;
	newentry->loglife = atoi(arg[2]);
      }
    } else {
      fprintf(stderr, "Unknown keyword '%s' in configfile at line %i\n", arg[0], linecounter);
      exit(1);
    }

    for (k=0; k<i+1; k++) {
      if (arg[k]) free (arg[k]);
    }
    free(arg);
  }
  fclose(f);

  /* do some defaults */
  if (!g->ticketfile) {
    char buff[1024];
    sprintf(buff, "/var/tmp/tkt_zlogger_%i", (int) getpid());
    g->ticketfile=mystrdup(buff);
  }
  if (!g->srvtab) g->srvtab="/etc/athena/srvtab";
  if (!g->kservice) g->kservice="rcmd";
  if (g->newtickets == 0) g->newtickets=6; /* 6 hours */
  if (g->newsubs < 5) g->newsubs=10; /* 10 minutes */
  if (!g->cells) g->cells="";
  
  return(0);
}


void argserr(char *key, int line) {
  fprintf(stderr, "Wrong number of arguments for '%s' on line %i in configfile\n", key, line);
  exit(1);
}


void keyerr(char *key, int line) {
  fprintf(stderr, "Found keyword '%s' before a 'define' on line %i in the config file\n", key, line);
  exit(1);
}


void regexerr(char *reg, int line) {
  fprintf(stderr, "Invalid regular expression %s on line %i in config file\n", reg, line);
  exit(1);
}
  

char **atokenize(char *buffer, char *sep, int *i) {
  char **args;
  char *workbuff, *foo;
  int done=0, first=1, count=0;

  workbuff=malloc(strlen(buffer)+1);
  memcpy(workbuff, buffer, strlen(buffer)+1);
  
  args=NULL;
  while (!done) {
    if (first) {
      first=0;
      foo=strtok(workbuff, sep);
    } else {
      foo=strtok(NULL, sep);
    }
    if (foo==NULL) {
      done=1;
    } else {
      args=(char **)realloc(args, sizeof(char *) * (count+1));
      args[count]=malloc(strlen(foo)+1);
      strcpy(args[count], foo);
      count++;
    }
  }
  *i=count;
  free(workbuff);
  return(args);
}


char *mystrdup(char *foo) {
  char *bar;
  bar=malloc(strlen(foo)+1);
  if (!bar) return((char *) NULL);
  strcpy(bar, foo);
  return((char *) bar);
}


void downstr(char *foo) {
  int i;
  for (i=0; foo[i]!=0; i++) {
    foo[i]=tolower(foo[i]);
  }
}


void get_tickets_and_tokens(globalconfig *g) {
  int err;
  char *buff;

  err=ksrvtgt(g->kservice, g->hostname, g->srvtab, (g->newtickets*20)+2);
  if(err!=0) {
    syslog(LOG_ERR, "ksrvtgt: %s (%d)", krb_get_err_text(err), err);
  } else {
    buff=malloc(strlen(g->cells)+100);
    sprintf(buff, "/bin/athena/aklog %s", g->cells);
    system(buff);
    free(buff);
  }
}


int ksrvtgt(char *principal, char *instance, char *in_srvtab, int timeunits) {
  char realm[REALM_SZ + 1];
  char srvtab[MAXPATHLEN + 1];

  memset(realm, 0, sizeof(realm));
  memset(srvtab, 0, sizeof(srvtab));
  
  if (!in_srvtab)
    strcpy(srvtab, KEYFILE);
  else
    strcpy(srvtab, in_srvtab);
  
  if (krb_get_lrealm(realm,1) != KSUCCESS)
    strcpy(realm, KRB_REALM);
  
  return(krb_get_svc_in_tkt(principal, instance, realm, "krbtgt",
                            realm, timeunits, srvtab));

}

void getsubs(globalconfig *g) {
  entry *tmpentry;
  ZSubscription_t sub;
  int ret;

  tmpentry = g->entries;
  while (tmpentry != NULL) {
    sub.zsub_class = tmpentry->class;
    sub.zsub_classinst = "*";
    sub.zsub_recipient = "*";
    if ((ret = ZSubscribeToSansDefaults(&sub,1,NULL)) != ZERR_NONE) {
      syslog(LOG_CRIT, "error %m getting subs");
    }
    tmpentry=tmpentry->next;
  }

}


void lognotice(globalconfig *g, ZNotice_t *notice) {
  entry *tmpentry;
  tmpentry = g->entries;

  if (!strcasecmp(notice->z_opcode, "ping")) return;

  /* gross, I know */
  while (tmpentry != NULL) {
    if (strcasecmp(tmpentry->class, notice->z_class)) {tmpentry = tmpentry->next; continue;}
    if (!tmpentry->negateinstance &&  regexec(&(tmpentry->instance), notice->z_class_inst, 0, NULL, 0)) {tmpentry = tmpentry->next; continue;}
    if ( tmpentry->negateinstance && !regexec(&(tmpentry->instance), notice->z_class_inst, 0, NULL, 0)) {tmpentry = tmpentry->next; continue;}
    if (!tmpentry->negateopcode &&  regexec(&(tmpentry->opcode), notice->z_opcode, 0, NULL, 0)) {tmpentry = tmpentry->next; continue;}
    if ( tmpentry->negateopcode && !regexec(&(tmpentry->opcode), notice->z_opcode, 0, NULL, 0)) {tmpentry = tmpentry->next; continue;}

    /* now check for nolog */
    if (tmpentry->nolog == 1) {
      if (!strcasecmp(notice->z_opcode, "nolog")) {tmpentry = tmpentry->next; continue;}
      if (!strcasecmp(notice->z_class_inst, "nolog")) {tmpentry = tmpentry->next; continue;}
    }

    /* if we made it this far go ahead and log it */
    writelog(tmpentry, notice);


    tmpentry=tmpentry->next;
  }
}


void writelog(entry *ent, ZNotice_t *notice) {
  char *time;
  struct hostent *hent;
  FILE *f;
  char *ptr, *message;
  int ret;

  message=malloc(notice->z_message_len+20);
  memcpy(message, notice->z_message, notice->z_message_len);
  memset(message + notice->z_message_len, 0,1);
  
  ptr=memchr(message, '\0', notice->z_message_len);
  if (!ptr) ptr=message;
  else ptr++;
  
  time=ctime((time_t *) &notice->z_time.tv_sec);
  memset(time+strlen(time)-1, 0, 1);
  
  hent=gethostbyaddr((void *) &(notice->z_sender_addr), sizeof(notice->z_sender_addr), AF_INET);

  downstr(notice->z_class);
  downstr(notice->z_class_inst);
  downstr(notice->z_opcode);

  if (!strcmp(notice->z_sender+(strlen(notice->z_sender)-strlen("@ATHENA.MIT.EDU")), "@ATHENA.MIT.EDU"))
    memset(notice->z_sender+(strlen(notice->z_sender)-strlen("@ATHENA.MIT.EDU")), 0, 1);

  
  if (ent->logfilenoparse) {
    /* if logfilenoparse==1 then we're class style logging */
    securefile(ent->logfile);
    f=fopen(ent->logfile, "a");
    if (!f) {
      syslog(LOG_ERR, "cannot open file for writing: %m");
    } else {
      ret=fprintf(f, "Instance: %s Time: %s", notice->z_class_inst, time);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, " Host: %s\n", (hent && hent->h_name) ? hent->h_name : inet_ntoa(notice->z_sender_addr));
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, "From: %s <%s>\n", notice->z_message, notice->z_sender);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, "\n%s\n",ptr);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fclose(f);
      if (ret) syslog(LOG_ERR, "error closing file: %m");
    }
  } else {
    /* it's instance style logging, figure out the filename */
    char replacement[1024];
    char newname[1024];
    char *foo;
      
    if (is_weird(notice->z_class_inst)) {
      strcpy(replacement, "weird");
    } else {
      strcpy(replacement, notice->z_class_inst);
    }

    foo=strstr(ent->logfile, "%i");
    /* !foo shouldn't happen since we setup logfilenoparse based on it in readconfig() */
    strncpy(newname, ent->logfile, foo-ent->logfile);
    newname[foo-ent->logfile] = '\0'; /* why do i have to do this !?!? */
    strcat(newname, replacement);
    if (ent->logfile+(foo-ent->logfile+2) != '\0') strcat(newname, ent->logfile+(foo-ent->logfile+2));

    securefile(newname);
    f=fopen(newname, "a");
    if (!f) {
      syslog(LOG_ERR, "cannot open file for writing: %m");
    } else {
      ret=fprintf(f, "Auth: %s ", notice->z_auth ? "yes" : "no");
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, " Time: %s", time);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, " Host: %s\n", (hent && hent->h_name) ? hent->h_name : inet_ntoa(notice->z_sender_addr));
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, "From: %s <%s>\n", notice->z_message, notice->z_sender);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      if (is_weird(notice->z_class_inst)) ret=fprintf(f, "Real Instance: %s\n", notice->z_class_inst);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fprintf(f, "\n%s\n",ptr);
      if (ret<0) syslog(LOG_ERR, "error writing to file: %m");
      ret=fclose(f);
      if (ret) syslog(LOG_ERR, "error closing file: %m");
    }
  }

  free(message);
}


int is_weird (char *instance) {
  int i, len, ch;

  /* between 1 and 25 characters */
  len=strlen(instance);
  if (len<1 || len>25) return(1);

  /* no /'s */
  if (strchr(instance, '/')) return(1);

  /* must start with [a-zA-Z0-9] */
  ch=instance[0];
  if (!isalnum(ch)) return(1);

  /* no unprintable characters */
  for (i=0; i<len; i++) {
    if (instance[i]<'!' || instance[i]>'~') return(1);
  }

  /* just to be sure ... should be covered above though */
  if (!strcmp(instance, ".") || !strcasecmp(instance, "..")) return(1);

  if (!strcasecmp(instance, "weird")) return(1);

  if (!strcasecmp(instance, "old")) return(1);
  
  return(0);
}


void turnoverlogs(globalconfig *g) {
  char old[1024], new[1024];
  entry *tmpentry;
  int i, fd;

  tmpentry = g->entries;
  
  while (tmpentry != NULL) {
    if (tmpentry->logroll == 0) {
      tmpentry=tmpentry->next;
      continue;
    }
    if (tmpentry->logfilenoparse == 0) {
      /* This is essentially a bug, we should do something about it in
         the future */
      tmpentry=tmpentry->next;
      continue;
    }
    
    for (i=tmpentry->loglife-1; i>=0; i--) {
      sprintf(old, "%s.%i", tmpentry->logfile, i);
      sprintf(new, "%s.%i", tmpentry->logfile, i+1);
      rename(old, new); /* ignoring errors for now */
    }
    securefile(tmpentry->logfile);
    sprintf(new, "%s.0", tmpentry->logfile);
    rename(tmpentry->logfile, new);
    fd=open(tmpentry->logfile, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    close(fd);

    tmpentry=tmpentry->next;
  }
}


void initlogs(globalconfig *g) {
  entry *tmpentry;
  int fd;
  
  tmpentry = g->entries;
  while (tmpentry != NULL) {
    if (tmpentry->logroll == 0) {
      tmpentry=tmpentry->next;
      continue;
    }
    if (tmpentry->logfilenoparse == 0) {
      /* same bug as above */
      tmpentry=tmpentry->next;
      continue;
    }
    fd=open(tmpentry->logfile, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    close(fd);
    tmpentry=tmpentry->next;
  }
  
}


void securefile(char *path) {
  struct stat sbuff;
  int ret;
  
  ret=lstat(path, &sbuff);
  if (ret) {
    if (errno == ENOENT) {
      return;
    } else {
      syslog(LOG_ERR, "%m doing lstat");
      return;
    }
  }

  if ((sbuff.st_mode & S_IFMT) != S_IFREG) {
    char logbuff[1024];

    /* if it's a link tell us where to, otherwise let us know it's weird */
    if ((sbuff.st_mode & S_IFMT) == S_IFLNK) {
      char link[1024];
      ret=readlink(path, link, 1024);
      if (ret==-1) strcpy(link, "an unknown file");
      sprintf(logbuff, "Found %s is a symlink to %s", path, link);
    } else if ((sbuff.st_mode & S_IFMT) == S_IFDIR) {
      sprintf(logbuff, "Found %s is a directory", path);
    } else {
      sprintf(logbuff, "Found %s is not a regular file", path);
    }
    syslog(LOG_WARNING, logbuff);

    /* try to unlink it */
    ret=unlink(path);
    if (ret) {
      sprintf(logbuff, "I was not able to unlink %s:%s", path, strerror(errno));
    } else {
      sprintf(logbuff, "unlinked %s", path);
    }
    syslog(LOG_WARNING, logbuff);
    
  }
}
