/* Copyright (C) 1998 Thorsten Kukuk
   Author: Thorsten Kukuk <kukuk@vt.uni-paderborn.de>

   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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

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

#include <crypt.h>
#include <locale.h>
#include <libintl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <rpcsvc/nis.h>

#include "log_msg.h"
#include "npd_yp.h"
#include "npd_nis.h"

#ifndef _
#define _(String) gettext (String)
#endif

struct npd_cache {
  bool_t in_use;
  time_t last_failed;
  u_int tries;
  char *username;
  char *oldpassword;
  nis_object *obj;
};

static struct npd_cache *cache;
static int cache_not_init = 1;

#define MAX_CACHE_ENTRIES 100

static nis_object *
get_userinfo (char *user)
{
  nis_result *result;
  nis_object *obj;
  struct dir_list *tmp_list = dirlist;

  if (user == NULL)
    return NULL;

  while (tmp_list != NULL)
    {
      char buf[strlen (user) + 24 + strlen (tmp_list->name)];

      sprintf (buf, "[name=%s],passwd.org_dir.%s", user, tmp_list->name);

      result = nis_list (buf, FOLLOW_PATH | FOLLOW_LINKS | EXPAND_NAME,
			 NULL, NULL);

      if (result == NULL || result->status != NIS_SUCCESS ||
	  __type_of (result->objects.objects_val) != NIS_ENTRY_OBJ)
	{
	  if (result)
	    nis_freeresult (result);
	  else
	    return NULL;
	}
      else
	{
	  obj = nis_clone_object (NIS_RES_OBJECT(result), NULL);
	  nis_freeresult (result);
	  return obj;
	}
      tmp_list = tmp_list->next;
    }
  return NULL;
}

static int
password_ok (char *plain, char *crypted)
{
  if (crypted[0] == '\0')
    return 1;
  if (strcmp (crypt (plain, crypted), crypted) == 0)
    return 1;

  return 0;
}

#define NISENTRYVAL(col,obj) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_value.ec_value_val)
#define NISENTRYLEN(col,obj) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_value.ec_value_len)
#define NISENTRYFLAG(col,obj) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_flags)

static bool_t
yppasswdproc_update_1_svc (yppasswd *argp, int *result, struct svc_req *rqstp)
{
  bool_t retval = TRUE;
  nis_object *obj;
  unsigned int idx, i;

  *result = 1;

  if (cache_not_init)
    {
      cache = calloc (1, MAX_CACHE_ENTRIES * sizeof (struct npd_cache));
      if (cache == NULL)
	{
	  log_msg (LOG_DEBUG, _("Out of memory!\n"));
	  return FALSE;
	}
      cache_not_init = 0;
    }

  if (debug_flag)
    {
      struct sockaddr_in *rqhost;

      rqhost = svc_getcaller (rqstp->rq_xprt);
      log_msg (LOG_DEBUG, "yppasswdproc_update() [From: %s:%d]",
               inet_ntoa (rqhost->sin_addr), ntohs (rqhost->sin_port));

      log_msg (LOG_DEBUG, "\t\tusername = \"%s\"", argp->newpw.pw_name);
    }

  obj = get_userinfo (argp->newpw.pw_name);
  if (obj == NULL)
    {
      log_msg (LOG_ERR, _("received YP update request for %s"),
               argp->newpw.pw_name);
      log_msg (LOG_ERR, _("no passwd entry exists for this user!"));
      return retval;
    }

  idx = (u_int)-1;
  for (i = 0; i < MAX_CACHE_ENTRIES; ++i)
    {
      if (cache[i].in_use == FALSE)
        {
          if (idx == (u_int)-1)
            idx = i; /* We don't stop, we could find an old try */
        }
      else
        if (strcmp (cache[i].username, argp->newpw.pw_name) == 0)
          {
            idx = i; /* Found an old try */
            break;
          }
    }

  if (idx == (u_int)-1 || idx >= MAX_CACHE_ENTRIES)
    {
      log_msg (LOG_ERR, _("received YP update request for %s"),
               argp->newpw.pw_name);
      log_msg (LOG_ERR, _("system error!"));
      return retval;
    }

  if (cache[idx].in_use)
    {
      nis_free_object (obj);
      obj = cache[idx].obj;
      free (cache[idx].oldpassword);
    }
  else
    {
      cache[idx].in_use = TRUE;
      cache[idx].tries = 0;
      cache[idx].username = strdup (argp->newpw.pw_name);
      cache[idx].obj = obj;
    }
  cache[idx].tries++;
  if (cache[idx].tries > (u_int)attempts)
    {
      time_t now;

      time (&now);
      if (cache[idx].last_failed + (minutes * 60) > now)
        {
          log_msg (LOG_ERR, _("received YP update request for %s"),
                   argp->newpw.pw_name);
          log_msg (LOG_ERR, _("too many failed attempts for %s"),
                   argp->newpw.pw_name);
          return retval;
        }
      else
        cache[idx].tries = 1;
    }
  time (&cache[idx].last_failed);

  cache[idx].oldpassword = strdup (argp->oldpass);

  /* Get the encrypted password and compare it */
  {
    char buffer[NISENTRYLEN (1, obj) + 1];

    strncpy (buffer, NISENTRYVAL (1, obj), NISENTRYLEN (1, obj));
    buffer [NISENTRYLEN (1, obj)] = '\0';
    if (!password_ok (argp->oldpass, buffer))
      {
	log_msg (LOG_ERR, _("received YP update request for %s"),
		 argp->newpw.pw_name);
	log_msg (LOG_ERR, _("password incorrect"));
	return retval;
      }
  }

  *result = 0;

  if (argp->newpw.pw_passwd != NULL &&
      strcmp (cache[idx].oldpassword, argp->newpw.pw_passwd) != 0)
    {
      char buf2[strlen (cache[idx].obj->zo_name) +
               strlen (cache[idx].obj->zo_domain) + 10];
      nis_result *res;

      obj = nis_clone_object (cache[idx].obj, NULL);

      free (NISENTRYVAL (1, obj));
      NISENTRYVAL (1, obj) = strdup (argp->newpw.pw_passwd);
      NISENTRYLEN (1, obj) = strlen (argp->newpw.pw_passwd) + 1;
      NISENTRYFLAG (1, obj) =
        NISENTRYFLAG (1, obj) | EN_MODIFIED;

      sprintf (buf2, "%s.%s", obj->zo_name, obj->zo_domain);
      res = nis_modify_entry (buf2, obj, MOD_SAMEOBJ);
      if (res->status != NIS_SUCCESS)
        {
          if (verbose)
            log_msg (LOG_ERR, "nis_modify_entry for passwd failed: %s",
                     nis_sperrno (res->status));
	  *result = 1;
        }
      nis_free_object (obj);
      nis_freeresult (res);
    }

  /* Change the gecos field */
  if (argp->newpw.pw_gecos != NULL &&
      strlen (argp->newpw.pw_gecos) > 0 &&
      strncmp (NISENTRYVAL (4, cache[idx].obj), argp->newpw.pw_gecos,
               NISENTRYLEN (4, cache[idx].obj)) != 0)
    {
      char buf2[strlen (cache[idx].obj->zo_name) +
               strlen (cache[idx].obj->zo_domain) + 10];
      nis_result *res;

      obj = nis_clone_object (cache[idx].obj, NULL);

      free (NISENTRYVAL (4, obj));
      NISENTRYVAL (4, obj) = strdup (argp->newpw.pw_gecos);
      NISENTRYLEN (4, obj) = strlen (argp->newpw.pw_gecos) + 1;
      NISENTRYFLAG (4, obj) =
        NISENTRYFLAG (4, obj) | EN_MODIFIED;

      sprintf (buf2, "%s.%s", obj->zo_name, obj->zo_domain);
      res = nis_modify_entry (buf2, obj, MOD_SAMEOBJ);
      if (res->status != NIS_SUCCESS)
        {
          if (verbose)
            log_msg (LOG_ERR, "nis_modify_entry for gecos field failed: %s",
                     nis_sperrno (res->status));
	  *result = 1;
        }
      nis_free_object (obj);
      nis_freeresult (res);
    }

  /* Change the shell field */
  if (argp->newpw.pw_shell != NULL &&
      strlen (argp->newpw.pw_shell) > 0 &&
      strncmp (NISENTRYVAL (6, cache[idx].obj), argp->newpw.pw_shell,
               NISENTRYLEN (6, cache[idx].obj)) != 0)
    {
      char buf2[strlen (cache[idx].obj->zo_name) +
               strlen (cache[idx].obj->zo_domain) + 10];
      nis_result *res;

      obj = nis_clone_object (cache[idx].obj, NULL);

      free (NISENTRYVAL (6, obj));
      NISENTRYVAL (6, obj) = strdup (argp->newpw.pw_shell);
      NISENTRYLEN (6, obj) = strlen (argp->newpw.pw_shell) + 1;
      NISENTRYFLAG (6, obj) =
        NISENTRYFLAG (6, obj) | EN_MODIFIED;

      sprintf (buf2, "%s.%s", obj->zo_name, obj->zo_domain);
      res = nis_modify_entry (buf2, obj, MOD_SAMEOBJ);
      if (res->status != NIS_SUCCESS)
        {
          if (verbose)
            log_msg (LOG_ERR, "nis_modify_entry for shell field failed: %s",
                     nis_sperrno (res->status));
	  *result = 1;
	}
      nis_free_object (obj);
      nis_freeresult (res);
    }

  free (cache[idx].username);
  free (cache[idx].oldpassword);
  nis_free_object (cache[idx].obj);
  cache[idx].in_use = FALSE;

  return retval;
}

static bool_t
xdr_xpasswd (XDR *xdrs, xpasswd *objp)
{
  return xdr_string(xdrs, &objp->pw_name, ~0)
    && xdr_string(xdrs, &objp->pw_passwd, ~0)
    && xdr_int(xdrs, &objp->pw_uid)
    && xdr_int(xdrs, &objp->pw_gid)
    && xdr_string(xdrs, &objp->pw_gecos, ~0)
    && xdr_string(xdrs, &objp->pw_dir, ~0)
    && xdr_string(xdrs, &objp->pw_shell, ~0);
}

static bool_t
xdr_yppasswd (XDR *xdrs, yppasswd *objp)
{
  return xdr_string(xdrs, &objp->oldpass, ~0)
    && xdr_xpasswd(xdrs, &objp->newpw);
}

void
yppasswdprog_1 (struct svc_req *rqstp, register SVCXPRT *transp)
{
  yppasswd argument;
  int result;
  bool_t retval;

  switch (rqstp->rq_proc)
    {
    case NULLPROC:
      svc_sendreply (transp, (xdrproc_t) xdr_void, NULL);
      return;

    case YPPASSWDPROC_UPDATE:
      break;

    default:
      svcerr_noproc (transp);
      return;
    }
  memset (&argument, 0, sizeof (argument));
  if (!svc_getargs (transp, (xdrproc_t) xdr_yppasswd, (caddr_t) &argument))
    {
      svcerr_decode (transp);
      return;
    }
  retval = yppasswdproc_update_1_svc (&argument, &result, rqstp);
  if (retval > 0 && !svc_sendreply (transp, (xdrproc_t) xdr_int,
				    (char *) &result))
    svcerr_systemerr (transp);
  if (!svc_freeargs (transp, (xdrproc_t) xdr_yppasswd, (caddr_t) &argument))
    {
      fprintf (stderr, "unable to free arguments");
      exit (1);
    }
  xdr_free ((xdrproc_t) xdr_int, (caddr_t) &result);

  return;
}
