#include <pwd.h>
#include <unistd.h>

#include <errno.h>

#include <crypt.h>

#include "buffer.h"
#include "byte.h"
#include "case.h"
#include "constmap.h"
#include "fd.h"
#include "pathexec.h"
#include "prot.h"
#include "sig.h"
#include "str.h"
#include "stralloc.h"
#include "wait.h"

#include "control.h"
#include "global.h"
#include "hmac_md5.h"
#include "md5.h"
#include "sha1.h"
#include "sha256.h"

#ifdef USE_CONFIG
  #include "fehsqm-config.h"
#else
  #include "auto_qmail.h"
  #include "hasspnam.h"
  #include "hasuserpw.h"
#endif

#ifdef HASGETSPNAM
  #include <shadow.h>
static struct spwd *spw;
#endif

#ifdef HASUSERPW
  #include <userpw.h>
static struct userpw *upw;
#endif

#define FDAUTH          3
#define FDGOSSIP        1
#define SOCKET_CALL     "-s"
#define DOVECOT_SERVICE "-x"
#define POP_USER        "qmail-pop3d"

/**
  @file qmail-authuser.c
  @return 0: ok
          1: credentials failure
          2: qmail-authuser is misused
        110: can't read controls
        111: temporary problem checking the password
*/

static struct passwd *pw;

char authbuf[512];
buffer ba = BUFFER_INIT(write, FDAUTH, authbuf, sizeof(authbuf));

struct constmap mapauthuser;
stralloc authfile = {0};
stralloc disabled = {0};
stralloc user = {0};  // user w/o domain appended
stralloc homedir = {0};
stralloc shell = {0};

/**
  @brief Supported storage methods:
  (1) authuser:[=]plainpasswd,
  (2) authuser:%hashpasswd,
  (3) authuser:?, authuser:!, *:?, *:! (! -> +environment)
  (4) x:+ -> checkvpw; x = { user@domain, @domain, @ } vmailmgr
  (5) x:& -> vchkpw; x = { user@domain, @domain, @ } vpopmail
  (6) x:= -> qmail-client; x = { user@domain, @domain, @ } dovecot
  Supported auth methods:
  user/login/plain: (1,2,3,4,5,6),
  cram-md5/apop: (1,5)
*/

static void exit(int fail)
{
  int i;

  for (i = 0; i < sizeof(authbuf); ++i) authbuf[i] = 0;
  _exit(fail);
}

static int dig_ascii(char *digascii, const char *digest, const int len)
{
  static const char hextab[] = "0123456789abcdef";
  int j;

  for (j = 0; j < len; j++) {
    digascii[2 * j] = hextab[digest[j] >> 4];
    digascii[2 * j + 1] = hextab[digest[j] & 0x0f];
  }
  digascii[2 * len] = '\0';

  return (2 * j);  // 2*len
}

static int auth_sha1(char *pwdhash, char *response)
{
  unsigned char digest[20];
  unsigned char digascii[41];

  sha1_hash(digest, response, str_len(response));
  dig_ascii(digascii, digest, 20);

  return str_diffn(digascii, pwdhash, 40);
}

static int auth_sha256(char *pwdhash, char *response)
{
  unsigned char digest[32];
  unsigned char digascii[65];

  sha256_hash(digest, response, str_len(response));
  dig_ascii(digascii, digest, 32);

  return str_diffn(digascii, pwdhash, 64);
}

static int auth_md5(char *pwdhash, char *response)
{
  MD5_CTX ctx;
  unsigned char digest[16];
  unsigned char digascii[33];

  MD5Init(&ctx);
  MD5Update(&ctx, response, str_len(response));
  MD5Final(digest, &ctx);
  dig_ascii(digascii, digest, 16);

  return str_diffn(digascii, pwdhash, 32);
}

static int auth_hash(char *password, char *response)
{
  switch (str_len(password)) {
    case 32: return auth_md5(password, response);
    case 40: return auth_sha1(password, response);
    case 64: return auth_sha256(password, response);
    default: return -1;
  }
}

static int auth_unix(char *user, char *response)
{
  char *encrypted = 0;
  char *stored = 0;
  int r = -1;

  pw = getpwnam(user);
  if (pw) {
    stored = pw->pw_passwd;
    if (!stralloc_copys(&homedir, pw->pw_dir)) exit(111);
    if (!stralloc_copys(&shell, pw->pw_shell)) exit(111);
  } else {
    if (errno == ETXTBSY) exit(111);
    exit(1);
  }

  if (response) {
#ifdef HASUSERPW
    upw = getuserpw(user);
    if (upw)
      stored = upw->upw_passwd;
    else if (errno == ETXTBSY)
      exit(111);
#elif defined(HASGETSPNAM)
    spw = getspnam(user);
    if (spw)
      stored = spw->sp_pwdp;
    else if (errno == ETXTBSY)
      exit(111);
#endif
    if (!stored || !*stored) exit(111);
    encrypted = crypt(response, stored);
    if (!encrypted) exit(111);  // no password given (tx. M.B.)
    r = str_diff(encrypted, stored);
  }

  if (r == 0 || !response) {
    if (prot_gid((int)pw->pw_gid) == -1) exit(1);
    if (prot_uid((int)pw->pw_uid) == -1) exit(1);
    if (chdir(pw->pw_dir) == -1) exit(111);
  }

  return r;
}

static int auth_apop(unsigned char *password, unsigned char *response, unsigned char *challenge)
{
  MD5_CTX context;
  unsigned char digest[16];
  unsigned char digascii[33];

  MD5Init(&context);
  MD5Update(&context, challenge, str_len(challenge));
  MD5Update(&context, password, str_len(password));
  MD5Final(digest, &context);
  dig_ascii(digascii, digest, 16);

  return (str_diff(digascii, response));
}

static int auth_cram(unsigned char *password, unsigned char *response, unsigned char *challenge)
{
  unsigned char digest[16];
  unsigned char digascii[33];

  hmac_md5(challenge, str_len(challenge), password, str_len(password), digest);
  dig_ascii(digascii, digest, 16);

  return (str_diff(digascii, response) && str_diff(password, response));
}

static int auth_dovecot(char *user, char *response, char *socket, char *service)
{
  int wstat;
  int child;
  char *wrapper[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
  int i = 0;

  close(FDGOSSIP); /* gossiping doveadm */

  switch (child = fork()) {
    case -1: exit(111);
    case 0:
      wrapper[i] = "doveadm";
      wrapper[++i] = "auth";
      wrapper[++i] = "test";
      if (socket) {
        wrapper[++i] = "-a";
        wrapper[++i] = socket;
      }
      if (service) {
        wrapper[++i] = "-x";
        wrapper[++i] = service;
      }
      wrapper[++i] = user;
      wrapper[++i] = response;
      wrapper[++i] = 0;

      execvp(wrapper[0], wrapper);
      exit(111);
  }

  if (wait_pid(&wstat, child) == -1) exit(111);
  if (wait_crashed(wstat)) exit(111);
  return wait_exitcode(wstat);
}

static int auth_wrapper(char *pam, char *arg1, char *arg2, char *auth, int len)
{
  int wstat;
  int child;
  int pi[2];
  char *wrapper[4] = {0, 0, 0, 0};

  if (pipe(pi) == -1) exit(111);
  if (pi[0] != FDAUTH) exit(111);

  switch (child = fork()) {
    case -1: exit(111);
    case 0:
      close(pi[1]);
      if (fd_copy(FDAUTH, pi[0]) == -1) exit(111);
      wrapper[0] = pam;
      wrapper[1] = arg1;
      wrapper[2] = arg2;
      wrapper[3] = 0;
      sig_pipedefault();

      execvp(wrapper[0], wrapper);
      exit(111);
  }
  close(pi[0]);

  buffer_init(&ba, write, pi[1], authbuf, sizeof(authbuf));
  if (buffer_put(&ba, auth, len) == -1) exit(111);
  if (buffer_flush(&ba) == -1) exit(111);
  close(pi[1]);

  if (wait_pid(&wstat, child) == -1) exit(111);
  if (wait_crashed(wstat)) exit(111);
  return wait_exitcode(wstat);
}

int main(int argc, char **argv)
{
  char *authuser;
  char *authpass;
  char *response = 0;
  char *challenge = 0;
  char *domain = 0;
  char *authsocket = 0;
  char *service = 0;
  char *maildirname = 0;
  int rc = -1; /* initialise: -1; ok: 0; !ok: > 0 */
  int authlen = 0;
  int buflen = 0;
  int domlen = 0;
  int popuser = 0;
  int i = 0;
  int r;

  if (!argv[1])
    exit(2);
  else if (argv[2]) {  // pop user with homedir
    if (!case_diffs(argv[1], POP_USER)) {
      if (!argv[3]) exit(2);
      maildirname = argv[2];
      popuser = 1;
    }
    if (!case_diffs(argv[1], SOCKET_CALL)) {  // dovecot socket
      if (!argv[3]) exit(2);
      authsocket = argv[2];
      if (!case_diffs(argv[3], DOVECOT_SERVICE)) {  // ++ dovecot service
        service = argv[4];
        if (!argv[5]) exit(2);
      }
    }
    if (!case_diffs(argv[1], DOVECOT_SERVICE)) {  // dovecot service
      if (!argv[3]) exit(2);
      service = argv[2];
      if (!case_diffs(argv[3], SOCKET_CALL)) {  // ++ dovecot socket
        if (!argv[5]) exit(2);
        authsocket = argv[4];
      }
    }
  }

  /* Read input on FDAUTH */

  for (;;) {
    do r = read(FDAUTH, authbuf + buflen, sizeof(authbuf) - buflen);
    while ((r == -1) && (errno == EINTR));
    if (r == -1) exit(111);
    if (r == 0) break;
    buflen += r;
    if (buflen >= sizeof(authbuf)) exit(2);
  }
  close(FDAUTH);

  authuser = authbuf + i; /* username */
  if (i == buflen) exit(2);
  while (authbuf[i++]) /* response */
    if (i == buflen) exit(2);
  response = authbuf + i;
  if (i == buflen) exit(2);
  while (authbuf[i++]) /* challenge */
    if (i == buflen) exit(2);
  challenge = authbuf + i;

  authlen = str_len(authuser);
  if (!stralloc_copyb(&user, authuser, authlen)) exit(111);

  if ((i = byte_rchr(authuser, authlen, '@'))) /* @domain */
    if (i < authlen && authuser[i] == '@') {
      domain = authuser + i;
      domlen = str_len(domain);
      case_lowerb(domain, domlen);
      user.len = 0;
      if (!stralloc_copyb(&user, authuser, i)) exit(111);
    }
  if (!stralloc_0(&user)) exit(111);

  /* Read control file users/authuser and go for checks */

  if (chdir(auto_qmail) == -1) exit(110);

  switch (control_readfile(&authfile, "users/authuser", 0)) {
    case -1: exit(110);
    case 0:
      if (!constmap_init(&mapauthuser, "", 0, 1)) exit(111);
    case 1:
      if (!constmap_init(&mapauthuser, authfile.s, authfile.len, 1)) exit(111);
  }

  /* Check for disabled authuser/domains */

  if (!stralloc_copys(&disabled, "!")) exit(111);
  if (!stralloc_catb(&disabled, authuser, authlen)) exit(111);
  if (constmap(&mapauthuser, disabled.s, disabled.len)) exit(1);

  if (domlen) {
    disabled.len = 0;
    if (!stralloc_copys(&disabled, "!")) exit(111);
    if (!stralloc_catb(&disabled, domain, domlen)) exit(111);
    if (constmap(&mapauthuser, disabled.s, disabled.len)) exit(1);
  }

  /* Virtual and system user accounts */

  authpass = constmap(&mapauthuser, authuser, authlen);

  if (!authpass && domlen) authpass = constmap(&mapauthuser, domain, domlen);  // 1. authuser accounts
  if (!authpass) authpass = constmap(&mapauthuser, "*", 1);                    // 2. system accounts
  if (!authpass) authpass = constmap(&mapauthuser, "@", 1);  // 3. virtual user accounts

  if (!authpass) exit(1);

  if (str_len(authpass) == 1) {
    switch (authpass[0]) {
      case '?': rc = auth_unix(user.s, response); break;
      case '+':
        if (popuser)
          rc = auth_wrapper("checkvpw", "qmail-pop3d", maildirname, authbuf, buflen);
        else
          rc = auth_wrapper("checkvpw", "true", "Maildir", authbuf, buflen);
        break;
      case '&':
        if (popuser)
          rc = auth_wrapper("vchkpw", "qmail-pop3d", maildirname, authbuf, buflen);
        else
          rc = auth_wrapper("vchkpw", "true", 0, authbuf, buflen);
        break;
      case '=': rc = auth_dovecot(authuser, response, authsocket, service); break;
      default:  rc = 2; break;
    }
  } else {
    switch (authpass[0]) {
      case '%': rc = auth_hash(authpass + 1, response); break;
      default:
        if (rc) {
          if (popuser) {
            if ((rc = auth_apop(authpass, response, challenge)) == 0) {
              auth_unix(user.s, 0);  // Unix environment only
            }
          } else
            rc = auth_cram(authpass, response, challenge);
        }
        break;
    }
  }

  if (rc) exit(rc);

  for (i = 0; i < sizeof(authbuf); ++i) authbuf[i] = 0;

  if (authsocket && service) pathexec(argv + 5);
  if (authsocket || service || popuser)
    pathexec(argv + 3);
  else
    pathexec(argv + 1);
  exit(111);
}