/*
 * Copyright (C) 1998 Caldera, Inc.
 * Copyright (C) 2003 Oswald Buddenhagen <ossi@kde.org>
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "kcheckpass.h"

#ifdef HAVE_PAM

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#ifdef HAVE_PAM_PAM_APPL_H
#include <pam/pam_appl.h>
#else
#include <security/pam_appl.h>
#endif

struct pam_data {
  char *(*conv) (ConvRequest, const char *);
  int abort:1;
  int classic:1;
};

#ifdef PAM_MESSAGE_NONCONST
typedef struct pam_message pam_message_type;
typedef void *pam_gi_type;
#else
typedef const struct pam_message pam_message_type;
typedef const void *pam_gi_type;
#endif

static int
PAM_conv (int num_msg, pam_message_type **msg,
	  struct pam_response **resp,
	  void *appdata_ptr)
{
  int count;
  struct pam_response *repl;
  struct pam_data *pd = (struct pam_data *)appdata_ptr;

  if (!(repl = calloc(num_msg, sizeof(struct pam_response))))
    return PAM_CONV_ERR;

  for (count = 0; count < num_msg; count++)
    switch (msg[count]->msg_style) {
    case PAM_TEXT_INFO:
      pd->conv(ConvPutInfo, msg[count]->msg);
      break;
    case PAM_ERROR_MSG:
      pd->conv(ConvPutError, msg[count]->msg);
      break;
    default:
      switch (msg[count]->msg_style) {
      case PAM_PROMPT_ECHO_ON:
        repl[count].resp = pd->conv(ConvGetNormal, msg[count]->msg);
        break;
      case PAM_PROMPT_ECHO_OFF:
        if (pd->classic) {
          // WARNING
          // This is far from foolproof, but it's the best we can do at this time...
          // Try to detect PIN entry requests
          if (strstr(msg[count]->msg, "PIN")) {
            repl[count].resp = pd->conv(ConvGetHidden, msg[count]->msg);
          }
          else {
            repl[count].resp = pd->conv(ConvGetHidden, 0);
          }
        }
        else {
          repl[count].resp = pd->conv(ConvGetHidden, msg[count]->msg);
        }
        break;
#ifdef PAM_BINARY_PROMPT
      case PAM_BINARY_PROMPT:
        repl[count].resp = pd->conv(ConvGetBinary, msg[count]->msg);
        break;
#endif
      default:
        /* Must be an error of some sort... */
        goto conv_err;
      }
      if (!repl[count].resp) {
        pd->abort = 1;
        goto conv_err;
      }
      repl[count].resp_retcode = PAM_SUCCESS;
      break;
    }
  *resp = repl;
  return PAM_SUCCESS;

 conv_err:
  for (; count >= 0; count--)
    if (repl[count].resp)
      switch (msg[count]->msg_style) {
      case PAM_PROMPT_ECHO_OFF:
	dispose(repl[count].resp);
        break;
#ifdef PAM_BINARY_PROMPT
      case PAM_BINARY_PROMPT: /* handle differently? */
#endif
      case PAM_PROMPT_ECHO_ON:
	free(repl[count].resp);
	break;
      }
  free(repl);
  return PAM_CONV_ERR;
}

static struct pam_data PAM_data;

static struct pam_conv PAM_conversation = {
  &PAM_conv,
  &PAM_data
};

#ifdef PAM_FAIL_DELAY
static void
fail_delay(int retval ATTR_UNUSED, unsigned usec_delay ATTR_UNUSED, 
	   void *appdata_ptr ATTR_UNUSED)
{}
#endif


AuthReturn Authenticate(const char *caller, const char *method,
        const char *user, char *(*conv) (ConvRequest, const char *))
{
  const char	*tty;
  pam_handle_t	*pamh;
  pam_gi_type	pam_item;
  const char	*pam_service;
  char		pservb[64];
  int		pam_error;

  openlog("kcheckpass", LOG_PID, LOG_AUTH);

  PAM_data.conv = conv;
  if (!strcmp(method, "classic")) {
    PAM_data.classic = 1;
    pam_service = caller;
  }
  else if (!strcmp(method, "pam")) {
    pam_service = caller;
  } else {
    sprintf(pservb, "%.31s-%.31s", caller, method);
    pam_service = pservb;
  }
  pam_error = pam_start(pam_service, user, &PAM_conversation, &pamh);
  if (pam_error != PAM_SUCCESS)
    return AuthError;

  tty = ttyname(0);
  if (!tty)
    tty = getenv ("DISPLAY");

  pam_error = pam_set_item (pamh, PAM_TTY, tty);
  if (pam_error != PAM_SUCCESS) {
    pam_end(pamh, pam_error);
    return AuthError;
  }

# ifdef PAM_FAIL_DELAY
  pam_set_item (pamh, PAM_FAIL_DELAY, (void *)fail_delay);
# endif

  pam_error = pam_authenticate(pamh, 0);
  if (pam_error != PAM_SUCCESS) {
    pam_end(pamh, pam_error);
    switch (pam_error) {
      case PAM_USER_UNKNOWN:
      case PAM_AUTH_ERR:
      case PAM_MAXTRIES: /* should handle this better ... */
      case PAM_AUTHINFO_UNAVAIL: /* returned for unknown users ... bogus */
        return AuthBad;
      case PAM_SERVICE_ERR:
        return AuthError;
      default:
        return AuthError;
    }
  }

  /* just in case some module is stupid enough to ignore a preset PAM_USER */
  pam_error = pam_get_item (pamh, PAM_USER, &pam_item);
  if (pam_error != PAM_SUCCESS) {
    pam_end(pamh, pam_error);
    return AuthError;
  }
  if (strcmp((const char *)pam_item, user)) {
    pam_end(pamh, PAM_SUCCESS); /* maybe use PAM_AUTH_ERR? */
    return AuthBad;
  }

  pam_error = pam_setcred(pamh, PAM_REFRESH_CRED);
  /* ignore errors on refresh credentials. If this did not work we use the old once. */

  pam_end(pamh, PAM_SUCCESS);
  return AuthOk;
}

#endif
