//  Copyright 2007-2010 Mir Calculate Ltd. http://www.calculate-linux.org
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
// 
//      http://www.apache.org/licenses/LICENSE-2.0
// 
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

#include <security/pam_modules.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <ctype.h>
#include <syslog.h>
#include <ldap.h>

#include <asm/unistd.h>

#define PAM_LDAP_PATH_CONF "/etc/ldap.conf"
#define PASSWD "/etc/passwd"
#define CACHE_PASSWD "/var/lib/calculate/calculate-client/cache/passwd"
#define PAM_SM_AUTH
#define MAX_V 30
#define WAITTIME 30
#define WAITTIMELDAP 60

typedef struct pam_config
{
  /* file name read from */
  char *configFile;
  /* space delimited list of servers */
  char *host;
  /* port, expected to be common to all servers */
  int port;
  int version;
  /* bind dn/pw for "anonymous" authentication */
  char *binddn;
  char *bindpw;

} pam_config_t;

static int
_alloc_config (pam_config_t ** presult)
{
  pam_config_t *result;

  if (*presult == NULL)
  {
    *presult = (pam_config_t *) calloc (1, sizeof (*result));
    if (*presult == NULL)
      return PAM_BUF_ERR;
  }

  result = *presult;
  result->configFile = NULL;
  result->host = NULL;
  result->port = 0;
  result->binddn = NULL;
  result->bindpw = NULL;
  result->version = LDAP_VERSION3;
  return PAM_SUCCESS;
}

static int
_read_config (const char *configFile, pam_config_t ** presult)
{
  /* this is the same configuration file as nss_ldap */
  FILE *fp;
  char b[BUFSIZ];
  pam_config_t *result;

  if (_alloc_config (presult) != PAM_SUCCESS)
  {
    return PAM_BUF_ERR;
  }

  result = *presult;

  /* configuration file location is configurable; default /etc/ldap.conf */
  if (configFile == NULL)
  {
    configFile = PAM_LDAP_PATH_CONF;
    result->configFile = NULL;
  }
  else
  {
    result->configFile = strdup (configFile);
    if (result->configFile == NULL)
      return PAM_BUF_ERR;
  }

  fp = fopen (configFile, "r");

  if (fp == NULL)
  {
    return PAM_SERVICE_ERR;
  }


  while (fgets (b, sizeof (b), fp) != NULL)
  {
    char *k, *v;
    int len;

    if (*b == '\n' || *b == '#')
      continue;

    k = b;
    v = k;
    while (*v != '\0' && *v != ' ' && *v != '\t')
      v++;

    if (*v == '\0')
      continue;

    *(v++) = '\0';

    /* skip all whitespaces between keyword and value */
    while (*v == ' ' || *v == '\t')
      v++;

    /* kick off all whitespaces and newline at the end of value */
    len = strlen (v) - 1;
    while (v[len] == ' ' || v[len] == '\t' || v[len] == '\n')
      --len;
    v[len + 1] = '\0';

    if (!strcasecmp (k, "host"))
    {
      result->host = strdup (v);
    }
    else if (!strcasecmp (k, "binddn"))
    {
      result->binddn = strdup (v);
    }
    else if (!strcasecmp (k, "bindpw"))
    {
      result->bindpw = strdup (v);
    }
    else if (!strcasecmp (k, "port"))
    {
      result->port = atoi (v);
    }
    else if (!strcasecmp (k, "ldap_version"))
    {
      result->version = atoi (v);
    }
  }

  if (result->host == NULL || result->binddn == NULL ||
      result->bindpw == NULL)
  {
      /* 
       * According to PAM Documentation, such an error in a config file
       * SHOULD be logged at LOG_ALERT level
       */
      syslog (LOG_ALERT, "pam_ldap: missing \"host\" in file \"%s\"",
              configFile);
      return PAM_SERVICE_ERR;
  }

  if (result->port == 0)
  {
    result->port = LDAP_PORT;
  }

  fclose (fp);

  /* can't use _pam_overwrite because it only goes to end of string, 
   * not the buffer
   */
  memset (b, 0, BUFSIZ);
  return PAM_SUCCESS;
}

static void
_release_config (pam_config_t ** pconfig)
{
  pam_config_t *c; 
  c = *pconfig;
  if (c == NULL)
    return;

  if (c->configFile != NULL)
    free (c->configFile);

  if (c->host != NULL)
    free (c->host);

  if (c->binddn != NULL)
    free (c->binddn);
  if (c->bindpw != NULL)
    free (c->bindpw);
}

static int
_check_ldap (int retry_count)
{
  int result = PAM_SERVICE_ERR;
  int timelimit = 3;
  struct berval userpw;
  struct berval *servcred;
  char buf[BUFSIZ];
  LDAP *ld=NULL;
  pam_config_t * config= NULL;
  if(_read_config(NULL,&config) != PAM_SUCCESS) {
    if(config)
      _release_config(&config);
    return PAM_SERVICE_ERR;
  }

  snprintf(buf,BUFSIZ,"ldap://%s:%d",config->host,config->port);
  if(ldap_initialize(&ld,buf) == LDAP_SUCCESS &&
     ldap_set_option(ld,LDAP_OPT_PROTOCOL_VERSION,
                     &config->version) == LDAP_SUCCESS &&
     ldap_set_option(ld,LDAP_OPT_TIMELIMIT,
                     &timelimit) == LDAP_SUCCESS) {
    userpw.bv_val = config->bindpw;
    userpw.bv_len = (userpw.bv_val != 0) ? strlen (userpw.bv_val) : 0;
    for(;retry_count;retry_count--) {
      if (ldap_sasl_bind_s(ld,config->binddn,LDAP_SASL_SIMPLE,
                           &userpw, NULL, NULL,&servcred) == LDAP_SUCCESS)  {
        result = PAM_SUCCESS;
        break;
      }
      else {
        result = PAM_TRY_AGAIN;
      }
      sleep(1);
    }
  }
  if(ld != NULL)
    ldap_unbind_ext(ld,NULL,NULL);
  _release_config(&config);
  return result;
}

int file_exists(const char *fname) {
  return access(fname, 0) != -1;
}

// serach user in passwd type file
int search_user(const char *username, char *filename) {
    char buf[BUFSIZ];
    FILE *fd = fopen(filename,"r");
    if(fd != NULL) {
        while( fgets(buf,BUFSIZ,fd) ) {
            char *tail = buf;
            char *token = strsep(&tail,":");
            if(token != NULL && strcmp(token,username) == 0) {
                fclose(fd);
                return 1;
            }
        }
        fclose(fd);
    }
    return 0;
}

// check: is local user?
int local_user(const char *username) {
    int pUser = search_user(username,PASSWD);
    int cUser = search_user(username,CACHE_PASSWD);
    // local user is user which found in /etc/passwd only
    return (pUser && !cUser)?1:0;
}

// Authentication function
PAM_EXTERN int pam_sm_authenticate(pam_handle_t * pamh, int flags
                                   ,int argc, const char **argv)
{
 int i;
 int retval;
 char *boot_client="/etc/runlevels/boot/client";
 char *default_client="/etc/runlevels/default/client";
 char *started_client="/run/openrc/started/client";
 char *started_local="/run/openrc/started/local";
 char *ldap_conf="/etc/ldap.conf";
 const char *login;

 // get username
 retval = pam_get_user(pamh, &login, "login: ");
 // don't wait ldap for authentificate local user
 if(retval == PAM_SUCCESS && login && local_user(login))
    return PAM_SUCCESS;

 // wait for client daemon
 if (file_exists(boot_client) || file_exists(default_client)) {
   for(i=0;i<WAITTIME;i++) {
       if(file_exists(started_client) || 
          file_exists(started_local)) {
            if(_check_ldap(WAITTIMELDAP-i)!=-1)
                return PAM_SUCCESS;
            break;
       }
       sleep(1);
   }
 }
 // break auth if not exists ldap.conf
 if (!file_exists(ldap_conf)) {
    return PAM_AUTHINFO_UNAVAIL;
 }
 return PAM_SUCCESS;
}

// Empty function, necessary for pam-module
PAM_EXTERN int pam_sm_setcred(pam_handle_t * pamh, int flags
                              ,int argc, const char **argv)
{
 return PAM_SUCCESS;
}

#ifdef PAM_STATIC
struct pam_module _pam_unix_auth_modstruct = {
    "pam_client",
    pam_sm_authenticate,
    pam_sm_setcred,
    NULL,
    NULL,
    NULL,
    NULL,
};
#endif
