/* passwd-db-inc.c: virtuser get/put for shadowlands forum.
 * To be conditionally included into passwd.c -- see that file for GPL text.
    Copyright (C) 1999-2008 Jeremy D. Monin, Dan Chin, Matt Lochansky
	(forum@shadowlands.org).
 */


#ifndef DEBUG_INS_AND_OUTS
#define DEBUG_INS_AND_OUTS 0
#endif

#if USE_VIRTUSERS

	/* Note that most of file is inside an IF USE_VIRTUSERS */


/* for open(2) */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/* for ndbm... holdover til autoconf is ready */
#ifdef JUST_DB_H
  #define DB_DBM_HSEARCH 1   /* for berkeley db4's ndbm-compatability */
  #include <db.h>
#else
  #ifdef NDBM_H_IN_GDBM_SUBDIR
    #include <gdbm/ndbm.h>
  #else
    #ifdef NDBM_H_IN_SUBDIR
      #include <db1/ndbm.h>
    #else
      #include <ndbm.h>
    #endif
  #endif
#endif

/* for rand */
#include <stdlib.h>

#else
typedef char datum;  /* dummy type to satisfy compiler */
#endif  /* USE_VIRTUSERS */

/* #-include "slfprefs.h" -- no need, passwd.c already includes this */

/* TODO: all the TODOs */

/* public:
   slf_virtuser_get_info
   slf_virtuser_put_info
   slf_virtuser_remove
   slf_virtuser_getfirst
   slf_virtuser_getnext
   slf_virtuser_getstop
   slf_cryptnewpw
   slf_virtuser_get_dbfname
   slf_virtuser_set_dbfname
*/


#if USE_VIRTUSERS

/* for unpack */
static char from_db_usrname    [32];
static char from_db_usrpw      [128];
static char from_db_usrgecos   [250];
static char from_db_usrsponsor [250];
static char from_db_usremail   [250];
static struct slf_userinfo from_db_user_info;  /* has pointers to the above */

/* for pack */
static char packed_db_user_info[SLF_VIRTUSER_DATASIZE];

#endif  /* USE_VIRTUSERS */

/* for db file selection */
static const char *dbfname=NULL;

const char *slf_virtuser_get_dbfname(void) { return dbfname; }

void slf_virtuser_set_dbfname(const char *newfname)
{
  if ((newfname == NULL) || (newfname[0] == '\0'))
    dbfname = NULL;
  else {
    dbfname = newfname;
#if DEBUG_INS_AND_OUTS
    Lprintf ("L74: slf_virtuser_set_dbfname ('%s');\n", newfname);
#endif
  }
}


#if USE_VIRTUSERS
#if DEBUG_INS_AND_OUTS
static void prnrec(datum dtu)
{
  int dlen = dtu.dsize;
  int i = 0;
  char *d = dtu.dptr;
  Lprintf ("\nsize is %d.", dlen);
  if (dlen > 160) {
    Lprintf ("** printing only first 160 bytes **");
    dlen = 160;
  }
  while (i < dlen)
  {
    /* in 16-byte chunks... */
    int j;
    char  *jp;
    Lprintf ("\n%4d [ ", i);
    for (j = 0, jp = d; (j+i<dlen)&&(j<16); ++j, ++jp)
    {
      if (j==8)
	Lprintf(" -");
      Lprintf (" %02x", (int) *jp);
    }
    Lprintf ("] ");
    for (j = 0, jp = d; (j+i<dlen)&&(j<16); ++j, ++jp)
      if ((*jp >= ' ') && (*jp <= (char)127))
	Lprintf ("%c", *jp);
      else
	Lprintf (".");

    i += 16;
    d += 16;
  }
  if (dlen < dtu.dsize)
    Lprintf("\n ** (%d more bytes not printed)", dtu.dsize - dlen);
  Lprintf ("\n\n");  
}
#endif /* DEBUG_INS_AND_OUTS */
#endif /* USE_VIRTUSERS */


#if USE_VIRTUSERS

/** given a buffer, pack the string "src" into it as a pascal string.
 *  Return a pointer to one past the last character of "src" in buf,
 *  or NULL if it won't fit or src or buf is NULL, or if src is greater
 *  than 255 chars long (unlikely in our application).
 */
static char* pack_pascal_string(char*buf, size_t bsiz, const char*src)
{
    size_t srclen;

    if (buf==NULL)
	return NULL;
    if (src==NULL)
    {
	*buf = '\0';  /* pretend it's 0-length */
	++buf;
	return buf;
    }

    srclen = strlen(src);
    if ((srclen >= (bsiz-1)) || (srclen > 255))
	return NULL;  /* won't fit (need room for length byte) */

    buf[0] = (char) srclen;   /* length byte */
    strncpy (&(buf[1]), src, srclen);  /* content */
    return &(buf[srclen + 1]);
}


/** given a packed pascal string, unpack it into destbuf as a C null-terminated
 *  string, and return a pointer to one past the end of that string, in the
 *  source buffer srcpstr.  Returns NULL if srcpstr=NULL,destbuf=NULL, or
 *  if it won't fit in destbuf.
 */
static char* unpack_pascal_string
    (char *srcpstr, char *destbuf, size_t destbsiz)
{
    size_t pstrsiz;

    if ((srcpstr==NULL)||(destbuf==NULL))
	return NULL;

    pstrsiz = (size_t) srcpstr[0];
    if (pstrsiz >= destbsiz)
	return NULL;  /* won't fit (need room for \0) */

    strncpy(destbuf, &(srcpstr[1]), pstrsiz);  /* content */
    destbuf[pstrsiz] = '\0';            /* null-terminate */
    return &(srcpstr[pstrsiz + 1]);
}


/** Something's failed; free the fields from the userdata struct.  
 *  (Don't call this unless 'dynalloc' is true!)
 */
static void freefields (struct slf_userinfo *pswd)
{
    /* casts to (char*) avoid GCC "discards const" warning */

    if (pswd->u.pw_name   !=NULL) free((char*)pswd->u.pw_name);
    if (pswd->u.pw_passwd !=NULL) free((char*)pswd->u.pw_passwd);
    if (pswd->u.pw_gecos  !=NULL) free((char*)pswd->u.pw_gecos);
    if (pswd->uv_sponsor  !=NULL) free((char*)pswd->uv_sponsor);
    if (pswd->uv_email    !=NULL) free((char*)pswd->uv_email);
}



/** Handle the basic unpacking common to both DB record format versions.
 *  Pass in *c pointing to the start of the 'common format' part of the rec.
 *  Returns the current *c after doing all this, or NULL if something failed.
 */
static char* do_unpack_basic
	( struct slf_userinfo *pswd, char *c, bool dynalloc)
{
  pswd->u_is_virtual = true;

  /* first byte is admin/nonadmin */
  if (((int) c[0]) == 1)
      pswd->u.pw_gid = admin_gid;
  else
      pswd->u.pw_gid = users_gid;
  ++c;

  /* name */
  c = unpack_pascal_string (c, from_db_usrname, sizeof(from_db_usrname));
  if (c == NULL)
      return NULL;  /* TODO TODO TODO error */

  /* passwd */
  c = unpack_pascal_string (c, from_db_usrpw, sizeof(from_db_usrpw));
  if (c == NULL)
      return NULL;  /* TODO TODO TODO error */


  /* gecos */
  c = unpack_pascal_string (c, from_db_usrgecos, sizeof(from_db_usrgecos));
  if (c == NULL)
      return NULL;


  /* now fill in the struct slf_userinfo (and its struct passwd) */

  if (! dynalloc) {
    pswd->u.pw_name   = from_db_usrname;
    pswd->u.pw_passwd = from_db_usrpw;
    pswd->u.pw_gecos  = from_db_usrgecos;
  } else {
    pswd->u.pw_name   = strdup(from_db_usrname);
    pswd->u.pw_passwd = strdup(from_db_usrpw);
    pswd->u.pw_gecos  = strdup(from_db_usrgecos);
    if ((pswd->u.pw_name==NULL) || (pswd->u.pw_passwd==NULL)
	||(pswd->u.pw_gecos==NULL))
    {
      freefields (pswd);
      return NULL;
    }
  }

  return c;
}


/* true for worked, false for didn't; called from passwd_struct_unpack.
 * this is for reading from DB records made before the 20010403 release.
 */
static bool do_unpack_v1
    (datum* rec, struct slf_userinfo *pswd, bool dynalloc)
{
  /* rec -> from_db_user_info */
  /* we set name, pw, gecos, gid (for admin or no) and leave the rest blank. */

  /* TODO TODO TODO currently assumes pristine, noncorrupted db record */
  char *c;
  c = (char *) (rec->dptr);  /* point c at start of db record */

#if DEBUG_INS_AND_OUTS
  Lprintf("L144: unpacking passwd from datum");
  prnrec(*rec);
#endif

  if (NULL == (c = do_unpack_basic( pswd, c, dynalloc )))
      return false;

  return true;
}  /* do_unpack_v1 */


/* true for worked, false for didn't; called from passwd_struct_unpack.
 * this is for reading DB records in the format of the 20010403 release.
 */
static bool do_unpack_v2
    (datum* rec, struct slf_userinfo *pswd, bool dynalloc)
{
  /* rec -> from_db_user_info */
  /* we set name, pw, gecos, gid (for admin or no) and leave the rest blank. */

  /* TODO TODO TODO currently assumes pristine, noncorrupted db record */
  char *c;
  c = (char *) (rec->dptr);  /* point c at start of db record */

#if DEBUG_INS_AND_OUTS
  Lprintf("L144: unpacking passwd from datum");
  prnrec(*rec);
#endif

  ++c; /* skip version byte */

  if (NULL == (c = do_unpack_basic( pswd, c, dynalloc )))
      return false;

  /** additional unpacking **/

  /* sponsor */   /* uv_sponsor */

  c = unpack_pascal_string (c, from_db_usrsponsor, sizeof(from_db_usrsponsor));
  if (c == NULL)
      return false;

     /* email addr */  /*uv_email */

  c = unpack_pascal_string (c, from_db_usremail, sizeof(from_db_usremail));
  if (c == NULL)
      return false;

  /* Done unpacking; move data around for caller's sake */

  if (! dynalloc) {
      if (from_db_usrsponsor [0] != '\0')
	  pswd->uv_sponsor = from_db_usrsponsor;
      if (from_db_usremail [0] != '\0')
	  pswd->uv_email   = from_db_usremail;
  } else {
      if (from_db_usrsponsor [0] != '\0')
      {
	  pswd->uv_sponsor = strdup(from_db_usrsponsor);
	  if (NULL == pswd->uv_sponsor)
	  {
	      freefields (pswd);
	      return false;
	  }
      }
      if (from_db_usrsponsor [0] != '\0')
      {
	  pswd->uv_email   = strdup(from_db_usremail);
	  if (NULL == pswd->uv_email)
	  {
	      freefields (pswd);
	      return false;
	  }
      }
  }

  return true;

}  /* do_unpack_v2 */

#endif /* USE_VIRTUSERS */


/* true for worked, false for didn't */
static bool passwd_struct_unpack
    (datum* rec, struct slf_userinfo *pswd, bool dynalloc)
{
#if USE_VIRTUSERS
    unsigned char *c;
    c = (unsigned char *) (rec->dptr);  /* point c at start of db record */

    /* zero mem to start */
    memset( pswd, 0, sizeof(struct slf_userinfo) );

    /* recognize version # */
    switch ( (int) c[0] )
    {
    case 0:
    case 1:
	return do_unpack_v1 (rec, pswd, dynalloc);
    case 2:  /* 20010403 */
	return do_unpack_v2 (rec, pswd, dynalloc);
    default:
        {
	    Lprintf ("ERROR: User database file is format version %d, but\n"
          "this versino of SLForum can only read up to %d\n", (int) c[0], 2);
	}
	return false;
    }
#else
    return false;
#endif /* USE_VIRTUSERS */
}



static void passwd_struct_pack
    (datum *rec, datum *key, const struct slf_userinfo *pack_from_here)
{
    /*  pack_from_here -> (rec,key) */
    /* out of:
          struct passwd {
	  char    *pw_name;
	  char    *pw_passwd;     
	  uid_t   pw_uid;         
	  gid_t   pw_gid;         
	  char    *pw_gecos;      
	  char    *pw_dir;        
	  char    *pw_shell;      
	  }; 
      we use name, passwd, gid, and gecos.  We also use the other slf_userinfo
      fields (uv_email, uv_sponsor, etc.)
      I've decided the packing will be pascal strings, believe it or not.
      [length byte followed by content]
      VERY first byte is version byte (0-255).  After that,
      if gid is admin_gid, the next byte in the record is 1, else it's 0.
      The other bytes are strings.
    */

#if USE_VIRTUSERS

    char *c, *newc;
    size_t buflen;

    if ((NULL == pack_from_here->u.pw_name)
	|| ('\0' == pack_from_here->u.pw_name[0]))
    {
	    Lprintf ("Called passwd_struct_pack with empty pw_name!\n");
	    return;
    }

    c = &(packed_db_user_info[0]);
    buflen = sizeof(packed_db_user_info);

    /* first byte marks it as record format version 2  (20010403) */
    c[0] = (char) 2;
    ++c;
    --buflen;

    /* admin? */
    if (pack_from_here->u.pw_gid == admin_gid)
	c[0] = (char) 1;
    else
	c[0] = (char) 0;

    ++c;
    --buflen;

    /* name */
    newc = pack_pascal_string(c, buflen, pack_from_here->u.pw_name);
    if (newc == NULL)
	return;  /* TODO TODO TODO error */
    buflen -= (newc - c);
    c = newc;

    /* passwd */
    newc = pack_pascal_string(c, buflen, pack_from_here->u.pw_passwd);
    if (newc == NULL)
	return;  /* TODO TODO TODO error */
    buflen -= (newc - c);
    c = newc;

    /* gecos */
    newc = pack_pascal_string(c, buflen, pack_from_here->u.pw_gecos);
    if (newc == NULL)
	return;  /* TODO TODO TODO error */
    c = newc;

    /* sponsor */
    newc = pack_pascal_string(c, buflen, pack_from_here->uv_sponsor);
    if (newc == NULL)
	return;  /* TODO TODO TODO error */
    c = newc;

    /* email */
    newc = pack_pascal_string(c, buflen, pack_from_here->uv_email);
    if (newc == NULL)
	return;  /* TODO TODO TODO error */
    c = newc;
    
    /* datum */
    rec->dptr  = packed_db_user_info;
    rec->dsize = (int) ( c - &(packed_db_user_info[0]) );

    /* and key */
    key->dptr  = pack_from_here->u.pw_name;
    key->dsize = strlen(pack_from_here->u.pw_name);

#if DEBUG_INS_AND_OUTS
  Lprintf("L266: packing passwd into datum.  key:");
  prnrec(*key);
  Lprintf("L266: packing passwd into datum.  rec:");
  prnrec(*rec);
#endif /* DEBUG_INS_AND_OUTS */
#endif /* USE_VIRTUSERS */
}


struct slf_userinfo *slf_virtuser_get_info
    (const char*name, const char*guess_of_pw, bool dynalloc)
{
#if USE_VIRTUSERS 

    DBM* dbf;
    datum getthis, dbrec;
    struct slf_userinfo *slfu=NULL;

    if (dbfname == NULL)
	return NULL;   /* not running */
    if ((name==NULL) || (strlen(name)==0))
	return NULL;   /* TODO: "root"/"slforum"/"guest" */
    if ((guess_of_pw!=NULL) && (strlen(guess_of_pw)==0))
	return NULL;   /* they must have a pw */

    /** flags, mode are same meaning as in open(2). */
    dbf = dbm_open(dbfname, O_RDONLY, 0700);
    if (dbf==NULL)
	return NULL;  /* TODO: error msg */

    getthis.dptr  = (char *) name;
    getthis.dsize = strlen(name);  /* TODO cache name strlen */

#if DEBUG_INS_AND_OUTS
    Lprintf ("L306: fetching record with key:\n");
    prnrec (getthis);
#endif

    dbrec = dbm_fetch (dbf, getthis);

    if (dbrec.dptr == NULL) {
#if DEBUG_INS_AND_OUTS
	Lprintf("L313: got NULL\n");
#endif
	return NULL;  /* not in DB */
    }

#if DEBUG_INS_AND_OUTS
    Lprintf ("L317: got datum:\n");
    prnrec (dbrec);
#endif

    /* copy fields from dptr to from_db_user_info after verifying size */
    if (!dynalloc)
      slfu = &from_db_user_info;
    else {
      slfu = (struct slf_userinfo *)malloc(sizeof(struct slf_userinfo));
      if (slfu==NULL) {
	/* TODO TODO TODO can we announce anything? */
	dbm_close(dbf);
	return NULL;
      }
    }
    if (!passwd_struct_unpack(&dbrec, slfu, dynalloc)) {
      /* TODO TODO TODO announce? */
      if(dynalloc) free(slfu);
      dbm_close(dbf);
      return NULL;
    }

    dbm_close(dbf);

    /* check password */
    if (guess_of_pw != NULL)
    {
      char salt[3];
      const char* crypted;

      salt[0] = slfu->u.pw_passwd[0];
      salt[1] = slfu->u.pw_passwd[1];
      salt[2] = '\0';
      crypted = crypt(guess_of_pw, salt);
	
      if (0 != strcmp(slfu->u.pw_passwd, crypted))
      {
	if (dynalloc) free (slfu);
	return NULL;  /* pw fails to match */
      }
    }

    return slfu;   /* they got it */
#else
    return NULL;
#endif /* USE_VIRTUSERS */
}



bool slf_virtuser_put_info (const struct slf_userinfo *udata)
{
#if USE_VIRTUSERS
    DBM* dbf;
    datum putthis, putkey;

    if (dbfname==NULL)
	return false;
    if (udata==NULL)
    {
	Lprintf ("passwd-db-inc L564: udata null\n");
	return false;
    }

    if (strlen(udata->u.pw_passwd) < 4)
    {
	Lprintf ("passwd-db-inc L570: must have a pw\n");
	return false;       /* must have a pw */
    }

    passwd_struct_pack(&putthis, &putkey, udata);

    dbf = dbm_open(dbfname, O_RDWR|O_CREAT, 0700);
    if (dbf==NULL)
    {
	Lprintf ("passwd-db-inc L579: dbm_open('%s') failed, errno %d\n",
		 dbfname, (int) errno);
	return false;  /* TODO: error msg to admin user? */
    }

    if (0 > dbm_store (dbf, putkey, putthis, DBM_REPLACE))
    {
      dbm_close(dbf);
      Lprintf ("passwd-db-inc L585: dbm_store failed\n");
      return false;
    }
    dbm_close(dbf);
    return true;
#else
    return false;
#endif /* USE_VIRTUSERS */
}



bool slf_virtuser_remove (const char *uname)
{
#if USE_VIRTUSERS
    DBM* dbf;
    datum delkey;

    if (dbfname == NULL)
	return false;
    if ((uname==NULL)||(uname[0] == '\0'))
	return true;  /* can't fail to do nothing */

    delkey.dptr = (char *) uname;
    delkey.dsize = strlen(uname);

    dbf = dbm_open(dbfname, O_RDWR|O_CREAT, 0700);
    if (dbf==NULL)
	return false;  /* TODO: error msg */

    if (0 != dbm_delete (dbf, delkey)) {    /* retval: 0 success, neg fail */
      /* TODO TODO TODO error msg if not success? */
      dbm_close(dbf);
      return false;
    }

    dbm_close (dbf);
    return true;
#else
    return false;
#endif /* USE_VIRTUSERS */
}


#if USE_VIRTUSERS
static DBM* gfn_dbf = NULL;   /* for getfirstnext */
#endif /* USE_VIRTUSERS */

const struct slf_userinfo *slf_virtuser_getfirst(void)
{
#if USE_VIRTUSERS 
  datum dbrec;

  if(dbfname==NULL)
  {
	  Lprintf ("getfirst: dbfname null");
	  return NULL;
  }

    /** flags, mode are same meaning as in open(2). */
    gfn_dbf = dbm_open(dbfname, O_RDONLY, 0700);
    if (gfn_dbf==NULL) {
#if DEBUG_INS_AND_OUTS
	Lprintf("L420: firstkey: could not open database ('%s'), errno %d",
		dbfname, (int) errno);
#endif
	return NULL;  /* TODO: error msg */
    }

    dbrec = dbm_firstkey (gfn_dbf);
    if (dbrec.dptr == NULL) {
#if DEBUG_INS_AND_OUTS
	Lprintf("L431: firstkey got NULL");
#endif
	return NULL;  /* DB empty */
    }

#if DEBUG_INS_AND_OUTS
    Lprintf("L433: firstkey got:\n");
    prnrec(dbrec);
#endif

    dbrec = dbm_fetch(gfn_dbf, dbrec);  /* get value for this key */
    if (dbrec.dptr == NULL)
      return NULL;  /* empty */

    /* copy fields from dptr to from_db_user_info after verifying size */
    passwd_struct_unpack(&dbrec, &from_db_user_info, false);
    return &from_db_user_info;
#else
    return NULL;
#endif /* USE_VIRTUSERS */
}


const struct slf_userinfo *slf_virtuser_getnext(void)
{
#if USE_VIRTUSERS
  datum dbrec;

  if (gfn_dbf == NULL)
    return NULL;  /* error msg? */

  dbrec = dbm_nextkey (gfn_dbf);
  if (dbrec.dptr == NULL) {
#if DEBUG_INS_AND_OUTS
    Lprintf("L455: nextkey got NULL\n");
#endif
    return NULL;  /* DB empty */
  }

#if DEBUG_INS_AND_OUTS
  Lprintf("L457: nextkey got:\n");
  prnrec(dbrec);
#endif

  dbrec = dbm_fetch(gfn_dbf, dbrec);  /* get value for this key */
  if (dbrec.dptr == NULL)
    return NULL;  /* empty */

  /* copy fields from dptr to from_db_user_info after verifying size */
  passwd_struct_unpack(&dbrec, &from_db_user_info, false);
  return &from_db_user_info;
#else
  return NULL;
#endif /* USE_VIRTUSERS */
}


void slf_virtuser_getstop (void)
{
#if USE_VIRTUSERS
  if (gfn_dbf != NULL)
    dbm_close(gfn_dbf);
  gfn_dbf = NULL;
#endif /* USE_VIRTUSERS */
}


const char *slf_cryptnewpw (const char *unenc_pw)
{
#if USE_VIRTUSERS
  static const char* saltstr
    = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./";
  char salt[3];

  salt[0] = saltstr[rand() % 64];
  salt[1] = saltstr[rand() % 64];
  salt[2] = '\0';

  return crypt (unenc_pw, salt);
#else
  return NULL;
#endif /* USE_VIRTUSERS */
}


