#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "md5.h"
#include <pwd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef HAVE_SHADOW_H
#include <shadow.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <errno.h>
#include <ctype.h>

#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif

#include "strlcpy.h"
#include "mysql.h"

extern const char * authfile;
char real_username[MAXLINE+1];
char real_maildrop[MAXLINE+1];
extern int use_pop3_allow_deny;

/* user_in_file() and is_user_allowed() by Lukasz Luzar
 * modified by Andreas Krennmair
 */
static int user_in_file(char * user, char * path) {
  int fd;
  size_t userlen;
  size_t len;
  char * buffer, * ptr;
  struct stat sb;

  /* sanity check */
  if (user==NULL || path==NULL) {
    return -1;
  }

  userlen = strlen(user);

  fd = open(path,O_RDONLY);
  if (fd < 0) {
    return -1;
  }
  if (0!=fstat(fd,&sb)) {
    close(fd);
    return -1;
  }
  len = (size_t)sb.st_size;
  buffer = alloca(len+1);

  /* sanity check */
  if (buffer==NULL) {
    return -1;
  }

  if (read(fd,buffer,(size_t)len)!=(ssize_t)len) {
    close(fd);
    return -1;
  }
  if (0!=close(fd)) {
    return -1;
  }

  buffer[len] = '\0';

  ptr = buffer;
  while (*ptr != '\0') {

    /* omit whitespaces */
    while (*ptr == '\t' || *ptr == ' ') {
      ptr++;
    }

    if (*ptr == '\0') {
      break;
    }

    if ((*ptr!='#') && (strncmp(ptr,user,userlen)==0) &&
        (ptr[userlen] == ' ' || ptr[userlen] == '\t' || ptr[userlen] == '\n')) {
      return 1;
    }

    while (*ptr != '\0' && *ptr != '\n') {
      ptr++;
    }

    if (*ptr == '\n') {
      ptr++;
    }
  }
  return 0;
}

static int is_user_allowed(char * user) {
  int allow, deny;

  switch (user_in_file(user,"/etc/pop3.allow")>0) {
    case 0:
      allow = 0;
      break;
    case 1:
      allow = 1;
      break;
    default: /* user_in_file() must not return anything else! */
      return 0;
  }

  switch (user_in_file(user,"/etc/pop3.deny")) {
    case 0:
      deny = 0;
      break;
    case 1:
      deny = 1;
      break;
    default: /* dito */
      return 0;
  }

  if ((allow == 0 && deny == 0) || (allow == 1 && deny == 0)) {
    return 1;
  }
  return 0;
}

/* returns a static buffer containing the string representation of an
 * MD5'ified password hash.  the string starts with "MD5-".  the rest
 * of the string is the lower case ASCII representation of an MD5 hash
 * (32 bytes).  the hash is calculated from the password followed by a
 * line feed (ASCII 10), followed by the user name, another line feed,
 * and the "magic" string "akpop3d". */
static char * calc_password_hash(char * username, char * password) {
  /* Sverre H. Huseby, 2003-05-09 */
  /* this program is not multi threaded, and we know we won't mix two
   * calls to the function, so let's make it simple using a static
   * return variable. */
  static char ret[4 + 32 + 1];
  static char * magic = "akpop3d";
  static char * hex_digits = "0123456789abcdef";
  char buf[1024];
  unsigned char md5[16];
  int q;
  char * p;
  unsigned char * up;

  if (strlen(username) + strlen(password) + strlen(magic) + 3 > sizeof(buf)) {
    syslog(LOG_ERR, "overly long username or password");
    return NULL;
  }
  snprintf(buf,sizeof(buf),"%s\n%s\n%s",password,username,magic);

  md5_buffer(buf, strlen(buf), md5);

  strlcpy(ret, "MD5-", sizeof(ret));
  p = ret + 4;
  up = md5;
  for (q = 0; q < 16; q++) {
    *p++ = hex_digits[*up >> 4];
    *p++ = hex_digits[*up & 0xf];
    ++up;
  }
  *p = '\0';

  return ret;
}

static int is_password_match(char * username, char * stored_password,
                             char * given_password) {
  /* Sverre H. Huseby, 2003-05-09 */
  char * hash;
  char * p1;
  char * p2;
  int q;

  if (strlen(stored_password) == (4 + 32)
      && strncmp(stored_password, "MD5-", 3) == 0) {
    /* the password is stored as and MD5 hash */
    hash = calc_password_hash(username, given_password);
    p1 = hash + 4;
    p2 = stored_password + 4;
    /* compare MD5 hashes represented as ASCII (32 bytes) , including
     * the terminating NUL byte (an additional byte).  could have used
     * strcasecmp, but as this function is not supported by all
     * systems, we do it the hard way. i guess i should have learned
     * to make those fancy autoconf rules, but i haven't. */
    for (q = 0; q < 32 + 1; q++) {
      /* tolower may be a macro I guess, so we can't increase the
       * p1/p2 pointers in the following if statement. */
      if (tolower(*p1) != tolower(*p2))
        return 0;
      ++p1;
      ++p2;
    }
    /* the hashes are equal. */
    return 1;
  } else {
    /* the password is stored in the clear.  yikes! */
    return (strcmp(given_password, stored_password) == 0);
  }
}

/* returns 0 if authentication failed, !0 if it succeeded. */
static int authenticate_by_file(char * username, char * password) {
  char * ptr;
  char linebuf[MAXLINE+1];
  FILE * fptr;

  if (authfile == NULL)
    return 0;

  fptr = fopen(authfile, "r");
  if (fptr == NULL) {
    syslog(LOG_ERR, "%s: %s: %s",
           "failed to read auth file", authfile, strerror(errno));
    return 0;
  }
  while (!ferror(fptr) && !feof(fptr)) {
    linebuf[0] = '\0';
    if (fgets(linebuf, sizeof(linebuf), fptr) == NULL) {
      fclose(fptr);
      return 0;
    }
    ptr = strtok(linebuf, ":");
    if (ptr == NULL)
      continue;
    if (strcmp(username, ptr) != 0)
      continue;
    ptr = strtok(NULL, ":");
    if (ptr == NULL) {
      fclose(fptr);
      return 0;
    }
    if (!is_password_match(username, ptr, password)) {
      fclose(fptr);
      return 0;
    }

    /*
     * At this point we've authenticated, but we now need to find out
     * what Unix username to use and what maildrop file to read
     */

    ptr = strtok(NULL, ":");
    if (ptr == NULL) {
      fclose(fptr);
      return 0;
    }

    strlcpy(real_username, ptr, sizeof(real_username));

    ptr = strtok(NULL, ":\n");
    if (ptr == NULL) {
      real_username[0] = '\0';
      fclose(fptr);
      return 0;
    }

    fclose(fptr);
    strlcpy(real_maildrop, ptr, sizeof(real_maildrop));

    return 1;
  }
  fclose(fptr);
  return 0;
}

int authenticate(char * username, char * password) {
  char user[MAXLINE+1], pass[MAXLINE+1];
  char * sys_pw;
  char * crp;
  struct passwd * u;
#ifdef HAVE_SHADOW_H
  struct spwd * s;
#endif
  int len;

  /* sanity checks */
  if (username==NULL || password==NULL) {
    return -1;
  }


  real_username[0] = '\0';
  real_maildrop[0] = '\0';

  /* extract username */
  crp = memchr(username,'\r',strlen(username));
  if (crp==NULL) {
    crp = memchr(username,'\n',strlen(username));
    if (crp==NULL) {
      crp = username + strlen(username);
    }
  }
  len = crp - username;
  if (len+1>sizeof(user)) {
    len = sizeof(user)-1;
  }
  memset(user,0,sizeof(user));
  strlcpy(user,username,len+1);

  /* extract password */
  crp = memchr(password,'\r',strlen(password));
  if (crp==NULL) {
    crp = memchr(password,'\n',strlen(password));
    if (crp==NULL) {
      crp = password + strlen(password);
    }
  }
  len = crp - password;
  if (len+1>sizeof(pass)) {
    len = sizeof(pass)-1;
  }
  memset(pass,0,sizeof(pass));
  strlcpy(pass,password,len+1);

  /* check /etc/pop3.{allow,deny} */
  if (0!=use_pop3_allow_deny && 0==is_user_allowed(user)) {
    return 0;
  }

  /*
   * Text-file authentication
   */
  if (authfile != NULL) {
    return authenticate_by_file(user, pass);
  }

#ifndef HAVE_LIBMYSQLCLIENT
  u = getpwnam(user);
#else
    u = getMpwnam( user ); /* getMpwnam first checks getpwnam() */
#endif /* HAVE_LIBMYSQLCLIENT */

  if (u == NULL || u->pw_passwd == NULL) {
    return -1;
  }

  /* handle shadowed passwd files */
#if HAVE_SHADOW_H
  if (strcmp(u->pw_passwd,"x")==0) {
#ifndef HAVE_LIBMYSQLCLIENT
    s = getspnam(user);
#else
    s = getMspnam(user); /* getMspanm first checks getspnam() */
#endif

    if (s==NULL) {
      return -1;
    }
    sys_pw = s->sp_pwdp;
  } else {
    sys_pw = u->pw_passwd;
  }
#else
  sys_pw = u->pw_passwd;
#endif

  if (strcmp(sys_pw,crypt(pass,sys_pw))==0) {
    return 1;
  }
  return 0;
}
