/* opieauto.c: The opieauto program.

%%% copyright-cmetz-96
This software is Copyright 1996-2001 by Craig Metz, All Rights Reserved.
The Inner Net License Version 3 applies to this software.
You should have received a copy of the license with this software. If
you didn't get a copy, you may request one from <license@inner.net>.

        History:

	Created by cmetz for OPIE 2.4 based on previously released
	        test code. Use opiestrncpy().
*/

#include "opie_cfg.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#if HAVE_SYS_TIME_H
#include <sys/time.h>
#endif /* HAVE_SYS_TIME_H */
#include <stdio.h>
#include <errno.h>
#if HAVE_STRING_H
#include <string.h>
#endif /* HAVE_STRING_H */
#include <getopt.h>
#if HAVE_STDLIB_H
#include <stdlib.h>
#endif /* HAVE_STDLIB_H */
#if HAVE_UNISTD_H
#include <unistd.h>
#endif /* HAVE_UNISTD_H */
#include <sys/stat.h>

#include "opie.h"

#ifndef max
#define max(x, y) (((x) > (y)) ? (x) : (y))
#endif /* max */

int window = 10;
char *myname = NULL;

uid_t myuid = 0;

#define MAXCLIENTS 2
int parents, s[MAXCLIENTS + 1];

char cmd[1+1+1+1+4+1+OPIE_SEED_MAX+1+4+1+4+1+4+1+4+1];

struct cachedotp {
  struct cachedotp *next;
  int algorithm, base, current;
  struct opie_otpkey basekey;
  char seed[OPIE_SEED_MAX+1];
};

struct cachedotp *head = NULL;

char *algids[] = { NULL, NULL, NULL, "sha1", "md4", "md5" };

void baile(x) {
  fprintf(stderr, "%s: %s: %s(%d)\n", myname, x, strerror(errno), errno);
  exit(1);
}

void bail(x) {
  fprintf(stderr, "%s: %s\n", myname, x);
  exit(1);
}

void zerocache(void)
{
  struct cachedotp *c = head, *c2;

  while(c) {
    c2 = c->next;
    memset(c, 0, sizeof(struct cachedotp));
    c = c2;
  };
};

int doreq(int fd)
{
  int algorithm, sequence, i;
  char *seed = NULL, *response = NULL;

  if (((cmd[0] != 'S') && (cmd[0] != 's')) || (cmd[1] != '=') || (cmd[2] != ' ')) {
#if DEBUG
    fprintf(stderr, "%s: got bogus command: %s\n", myname, cmd);
#endif /* DEBUG */
    goto error;
  };

  {
  char *c;

  if (((algorithm = strtoul(&cmd[3], &c, 10)) < 3) || (algorithm > 5) || (*c != ' ')) {
#if DEBUG
    fprintf(stderr, "%s: got bogus algorithm: %s\n", myname, cmd);
#endif /* DEBUG */
    goto error;
  };

  if (((sequence = strtoul(c + 1, &c, 10)) <= OPIE_SEQUENCE_RESTRICT) || (sequence > OPIE_SEQUENCE_MAX)) {
#if DEBUG
    fprintf(stderr, "%s: got bogus sequence: %s\n", myname, cmd);
#endif /* DEBUG */
    goto error;
  };

  if (cmd[0] == 'S') {
    if (!(c = strchr(seed = c + 1, ' '))) {
#if DEBUG
      fprintf(stderr, "%s: got bogus seed: %s\n", myname, cmd);
#endif /* DEBUG */
      goto error;
    };

    *c = 0;

    if (!(c = strchr(response = c + 1, '\n'))) {
#if DEBUG
      fprintf(stderr, "%s: got bogus response: %s\n", myname, cmd);
#endif /* DEBUG */
      goto error;
    };

    *c = 0;
  } else {
    if (!(c = strchr(seed = c + 1, '\n'))) {
#if DEBUG
      fprintf(stderr, "%s: got bogus seed: %s\n", myname, cmd);
#endif /* DEBUG */
      goto error;
    };

    *c = 0;
  };
  };

#if DEBUG
  fprintf(stderr, "got cmd=%c, algorithm=%d sequence=%d seed=+%s+ response=+%s+ on fd %d\n", cmd[0], algorithm, sequence, seed, response, fd);
#endif /* DEBUG */

  seed = strdup(seed);

  if (sequence < 10) {
#if DEBUG
    fprintf(stderr, "sequence < 10; can't do it\n");
#endif /* DEBUG */
    sprintf(cmd, "%c- %d %d %s\n", cmd[0], algorithm, sequence, seed);
  };

  {
  struct cachedotp **c;

  for (c = &head; *c && (strcmp((*c)->seed, seed) || ((*c)->algorithm != algorithm)); c = &((*c)->next));
  if (!(*c)) {
    if (cmd[0] == 's') {
#if DEBUG
      fprintf(stderr, "(seed, algorithm) not found for s command\n");
#endif /* DEBUG */
      sprintf(cmd, "s- %d %d %s\n", algorithm, sequence, seed);
      goto out;
    }

    if (!(*c = malloc(sizeof(struct cachedotp))))
      baile("malloc");
    memset(*c, 0, sizeof(struct cachedotp));

    (*c)->algorithm = algorithm;
    opiestrncpy((*c)->seed, seed, OPIE_SEED_MAX);
  };

  if (cmd[0] == 'S') {
    (*c)->base = max(sequence - window + 1, OPIE_SEQUENCE_RESTRICT);
    (*c)->current = sequence;

    if (!opieatob8(&(*c)->basekey, response))
      goto error;

    sprintf(cmd, "S+ %d %d %s\n", algorithm, sequence, (*c)->seed);
  } else {
    if (sequence != ((*c)->current - 1)) {
#if DEBUG
      fprintf(stderr, "out of sequence: sequence=%d, base=%d, current=%d\n", sequence, (*c)->base, (*c)->current);
#endif /* DEBUG */
      sprintf(cmd, "s- %d %d %s\n", algorithm, sequence, (*c)->seed);
      goto out;
    };

    if (sequence < (*c)->base) {
#if DEBUG
      fprintf(stderr, "attempt to generate below base: sequence=%d, base=%d, current=%d\n", sequence, (*c)->base, (*c)->current);
#endif /* DEBUG */
      sprintf(cmd, "s- %d %d %s\n", algorithm, sequence, (*c)->seed);
      goto out;
    };

    (*c)->current = sequence;
    i = sequence - (*c)->base;
    {
      struct opie_otpkey key;
      char buffer[16+1];

      key = (*c)->basekey;
      while(i--)
	opiehash(&key, algorithm);

      opiebtoa8(buffer, &key);
      sprintf(cmd, "s+ %d %d %s %s\n", algorithm, sequence, (*c)->seed, buffer);
    };
  };

  printf("%c otp-%s %d %s (%d/%d)\n", cmd[0], algids[algorithm], sequence, (*c)->seed, sequence - (*c)->base, window);
  fflush(stdout);

  if (sequence == (*c)->base) {
    struct cachedotp *c2 = *c;
    *c = (*c)->next;
    memset(c2, 0, sizeof(struct cachedotp));
    free(c2);
  };
  };

out:
  write(fd, cmd, i = strlen(cmd));
  free(seed);
  return 0;

error:
  fprintf(stderr, "Invalid command on fd %d\n", fd);
  if (seed)
    free(seed);
  return -1;
}

static void usage()
{
  fprintf(stderr, "usage: %s [-v] [-h] [-q] [-n <number of OTPs>]\n", myname);
  exit(1);
}

int main(int argc, char **argv)
{
  int i;
  struct stat st;
  char *sockpath;

  if (myname = strrchr(argv[0], '/'))
    myname++;
  else
    myname = argv[0];

  while((i = getopt(argc, argv, "w:hv")) != EOF) {
    switch(i) {
      case 'v':
	opieversion();

      case 'w':
	if (!(window = atoi(optarg))) {
	  fprintf(stderr, "%s: invalid number of OTPs: %s\n", myname, optarg);
	  exit(1);
	};
	break;

      default:
	usage();
    }
  };

  {
    uid_t myeuid;

    if (!(myuid = getuid()) || !(myeuid = geteuid()) || (myuid != myeuid))
      bail("this program must not be run with superuser priveleges or setuid.");
  };

  if (atexit(zerocache) < 0)
    baile("atexit");

  {
    struct sockaddr_un sun;

    memset(&sun, 0, sizeof(struct sockaddr_un));
    sun.sun_family = AF_UNIX;

    {
    char *c;
    char *c2 = "/.opieauto";

    if (!(c = getenv("HOME")))
      bail("getenv(HOME) failed -- no HOME variable?");

    if (strlen(c) > (sizeof(sun.sun_path) - strlen(c2) - 1))
      bail("your HOME is too long");

    strcpy(sun.sun_path, c);
    strcat(sun.sun_path, c2);
    sockpath = strdup(sun.sun_path);
    };

    if ((parents = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
      baile("socket");

    if (unlink(sockpath) && (errno != ENOENT))
      baile("unlink");

    if (umask(0177) < 0)
      baile("umask");

    if (bind(parents, (struct sockaddr *)&sun, sizeof(struct sockaddr_un)))
      baile("bind");

    if (stat(sockpath, &st) < 0)
      baile("stat");

    if ((st.st_uid != myuid) || (!S_ISSOCK(st.st_mode)) || ((st.st_mode & 07777) != 0600))
      bail("socket permissions and/or ownership were not correctly created.");

    if (listen(parents, 1) < 0)
      baile("listen");
  };

  {
    fd_set fds, rfds, efds;
    int maxfd = parents;
    int i, j;

    FD_ZERO(&fds);
    FD_SET(parents, &fds);

    while(1) {
      memcpy(&rfds, &fds, sizeof(fd_set));

      if (select(maxfd + 1, &rfds, NULL, NULL, NULL) < 0)
	baile("select");

      for (i = 0; s[i]; i++) {
	if (!FD_ISSET(s[i], &rfds))
	  continue;

	if (((j = read(s[i], cmd, sizeof(cmd)-1)) <= 0) || ((cmd[j] = 0) || doreq(s[i]))) {
	  close(s[i]);
	  FD_CLR(s[i], &fds);

	  if (s[i] == maxfd)
	    maxfd--;

	  for (j = i; s[j]; s[j] = s[j + 1], j++);
	  FD_SET(parents, &fds);
	  i--;
	  continue;
	};
      };

      if (FD_ISSET(parents, &rfds)) {
	for (i = 0; s[i]; i++)
	  if (i > MAXCLIENTS)
	    bail("this message never printed");

	if (stat(sockpath, &st) < 0)
	  baile("stat");

	if ((st.st_uid != myuid) || (!S_ISSOCK(st.st_mode)) || ((st.st_mode & 07777) != 0600))
	  bail("socket permissions and/or ownership has been messed with.");

	if ((s[i] = accept(parents, NULL, 0)) < 0)
	  baile("accept");

	FD_SET(s[i], &fds);
	if (s[i] > maxfd)
	  maxfd = s[i];

	sprintf(cmd, "C+ %d\n", window);
	if (write(s[i], cmd, j = strlen(cmd)) != j)
	  baile("write");

	if (++i == MAXCLIENTS)
	  FD_CLR(parents, &fds);
      }
    }
  }
}