/*
 * (c) Copyright 1995 HEWLETT-PACKARD COMPANY
 * 
 * To anyone who acknowledges that this file is provided 
 * "AS IS" without any express or implied warranty:
 * permission to use, copy, modify, and distribute this 
 * file for any purpose is hereby granted without fee, 
 * provided that the above copyright notice and this 
 * notice appears in all copies, and that the name of 
 * Hewlett-Packard Company not be used in advertising or 
 * publicity pertaining to distribution of the software 
 * without specific, written prior permission.  Hewlett-
 * Packard Company makes no representations about the 
 * suitability of this software for any purpose.
 *
 */
/*
 * k5dcecon - Program to convert a K5 TGT to a DCE context,
 * for use with DFS and its PAG.
 * 
 * The program is designed to be called as a sub process, 
 * and return via stdout the name of the cache which implies 
 * the PAG which should be used. This program itself does not 
 * use the cache or PAG itself, so the PAG in the kernel for 
 * this program may not be set. 
 * 
 * The calling program can then use the name of the cache
 * to set the KRB5CCNAME and PAG for itself and its children. 
 *
 * If no ticket was passed, an attemplt to join an existing
 * PAG will be made. 
 * 
 * If a forwarded K5 TGT is passed in, either a new DCE 
 * context will be created, or an existing one will be updated.
 * If the same ticket was already used to create an existing
 * context, it will be joined instead. 
 * 
 * Parts of this program are based on k5dceauth,c which was
 * given to me by HP and by the k5dcelogin.c which I developed. 
 * A slightly different version of k5dcelogin.c, was added to
 * DCE 1.2.2
 * 
 * D. E. Engert 6/17/97 ANL
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <locale.h>
#include <pwd.h>
#include <string.h>
#include <time.h>

#include <errno.h>
#include "k5dce.h"

#include <dce/sec_login.h>
#include <dce/dce_error.h>
#include <dce/passwd.h>

/* #define DEBUG */
#if defined(DEBUG)
#define DEEDEBUG(A) fprintf(stderr,A); fflush(stderr)
#define DEEDEBUG2(A,B) fprintf(stderr,A,B); fflush(stderr)
#else
#define DEEDEBUG(A)
#define DEEDEBUG2(A,B)
#endif

#ifdef __hpux
#define seteuid(A)		setresuid(-1,A,-1)
#endif


int k5dcecreate (uid_t, char *, char*, krb5_creds **);
int k5dcecon (uid_t, char *, char *);
int k5dcegettgt (krb5_ccache *, char *, char *, krb5_creds **);
int k5dcematch (uid_t, char *, char *, off_t *, krb5_creds **);
int k5dcesession (uid_t, char *, krb5_creds **, int *,krb5_flags);


char *progname = "k5dcecon";
static time_t now;

#ifdef notdef
#ifdef _AIX
/*---------------------------------------------*/
 /* AIX with DCE 1.1 does not have the com_err in the libdce.a
  * do a half hearted job of substituting for it. 
  */ 
void com_err(char *p1, int code, ...) 
{
    int lst;
    dce_error_string_t  err_string;
    dce_error_inq_text(code, err_string, &lst);
    fprintf(stderr,"Error %d in %s: %s\n", code, p1, err_string );
}

/*---------------------------------------------*/
void krb5_init_ets()
{

}
#endif
#endif


/*------------------------------------------------*/
/* find a cache to use  for our new pag           */
/* Since there is no simple way to determine which
 * caches are associated with a pag, we will have
 * do look around and see what makes most sense on 
 * different systems. 
 * on a Solaris system, and in the DCE source, 
 * the pags always start with a 41. 
 * this is not true on the IBM, where there does not
 * appear to be any pattern. 
 * 
 * But since we are always certifing our creds when
 * they are received, we can us that fact, and look
 * at the first word of the associated data file
 * to see that it has a "5". If not don't use. 
 */

int k5dcesession(luid, pname, tgt, ppag, tflags)
  uid_t luid;
  char *pname;
  krb5_creds **tgt;
  int *ppag;
  krb5_flags tflags;
{
  DIR *dirp;
  struct dirent *direntp;
  off_t size;
  krb5_timestamp endtime;
  int better = 0;
  krb5_creds *xtgt;

  char prev_name[17] = "";  
  krb5_timestamp prev_endtime;
  off_t prev_size;
  u_long prev_pag = 0;

  char ccname[64] = "FILE:/opt/dcelocal/var/security/creds/";
 
  error_status_t st;
  sec_login_handle_t lcontext = 0;
  dce_error_string_t  err_string;
  int lst;

  DEEDEBUG2("k5dcesession looking for flags %8.8x\n",tflags);

  dirp = opendir("/opt/dcelocal/var/security/creds/");
  if (dirp == NULL) {
	return 1;
  }

  while ( (direntp = readdir( dirp )) != NULL ) {

/*  
 * (but root has the ffffffff which we are not interested in)
 */
    if (!strncmp(direntp->d_name,"dcecred_",8)
         && (strlen(direntp->d_name) == 16)) {

      /* looks like a cache name, lets do the stat, etc */

      strcpy(ccname+38,direntp->d_name);
      if (!k5dcematch(luid, pname, ccname, &size, &xtgt))  {

        /* it's one of our caches, see if it is better  
         * i.e. the endtime is farther, and if the endtimes
         * are the same, take the larger, as he who has the 
         * most tickets wins.
         * it must also had the same set of flags at least
         * i.e. if the forwarded TGT is forwardable, this one must 
         * be as well.   
         */

        DEEDEBUG2("Cache:%s",direntp->d_name);
        DEEDEBUG2(" size:%d",size);
		DEEDEBUG2(" flags:%8.8x",xtgt->ticket_flags);
		DEEDEBUG2(" %s",ctime((time_t *)&xtgt->times.endtime));
 
        if ((xtgt->ticket_flags & tflags) == tflags ) {
          if (prev_name[0]) {
            if (xtgt->times.endtime > prev_endtime) {
              better = 1;
            } else if ((xtgt->times.endtime = prev_endtime) 
                  && (size > prev_size)){
              better = 1;
	        }
          } else {   /* the first */
            if (xtgt->times.endtime >= now) {
            better = 1;
	        }
          }
          if (better) {
            strcpy(prev_name, direntp->d_name);
	  	    prev_endtime = xtgt->times.endtime;
            prev_size = size;
            sscanf(prev_name+8,"%8X",&prev_pag);
			*tgt = xtgt;
            better = 0;
          }
        }
      } 
    }
  }
  (void)closedir( dirp );

  if (!prev_name[0])  
	 return 1; /* failed to find one */

   DEEDEBUG2("Best: %s\n",prev_name);

   if (ppag)
      *ppag = prev_pag;

   strcpy(ccname+38,prev_name);
   setenv("KRB5CCNAME",ccname,1);
 
   return(0);
}


/*----------------------------------------------*/
/* see if this cache is for this this principal */

int k5dcematch(luid, pname, ccname, sizep, tgt) 
  uid_t luid;
  char *pname;
  char *ccname;
  off_t *sizep;  /* size of the file */
  krb5_creds **tgt;
{

  krb5_ccache cache;
  struct stat stbuf;
  char ccdata[256];
  int fd;
  int status;

  /* DEEDEBUG2("k5dcematch called: cache=%s\n",ccname+38); */

  if (!strncmp(ccname,"FILE:",5)) {

    strcpy(ccdata,ccname+5);
    strcat(ccdata,".data");

    /* DEEDEBUG2("Checking the .data file for %s\n",ccdata); */

    if (stat(ccdata, &stbuf))
      return(1);
 
    if (stbuf.st_uid != luid)
      return(1);

    if ((fd = open(ccdata,O_RDONLY)) == -1)
      return(1);
    
    if ((read(fd,&status,4)) != 4) {
      close(fd);
      return(1);
    }
    
    /* DEEDEBUG2(".data file status = %d\n", status); */

    if (status != 5)
     return(1);

    if (stat(ccname+5, &stbuf))
      return(1);

    if (stbuf.st_uid != luid)
      return(1);

    *sizep = stbuf.st_size;
  }

  return(k5dcegettgt(&cache, ccname, pname, tgt));
}


/*----------------------------------------*/
/* k5dcegettgt - get the tgt from a cache */

int k5dcegettgt(pcache, ccname, pname, tgt)
  krb5_ccache *pcache;
  char *ccname;
  char *pname;
  krb5_creds **tgt;

{
  krb5_ccache cache;
  krb5_cc_cursor cur;
  krb5_creds creds;
  int code;
  int found = 1;
  krb5_principal princ;
  char *kusername;
  krb5_flags flags;
  char *sname, *realm, *tgtname = NULL;

  /* Since DCE does not expose much of the Kerberos interface,
   * we will have to use what we can. This means setting the 
   * KRB5CCNAME for each file we want to test
   * We will also not worry about freeing extra cache structures
   * as this this routine is also not exposed, and this should not 
   * effect this module. 
   * We should also free the creds contents, but that is not exposed
   * either. 
   */

  setenv("KRB5CCNAME",ccname,1);
  cache = NULL;
  *tgt = NULL;

  if (code = krb5_cc_default(pcache)) {
     com_err(progname, code, "while getting ccache");
     goto return2;
  }

  DEEDEBUG("Got cache\n");
  flags = 0;
  if (code = krb5_cc_set_flags(*pcache, flags)) {
    com_err(progname, code,"While setting flags"); 
    goto return2;
  }
  DEEDEBUG("Set flags\n");
  if (code = krb5_cc_get_principal(*pcache, &princ)) {
	com_err(progname, code, "While getting princ");
    goto return1;
  }
  DEEDEBUG("Got principal\n");
  if (code = krb5_unparse_name(princ, &kusername)) {
    com_err(progname, code, "While unparsing principal");
    goto return1;
  }

  DEEDEBUG2("Unparsed to \"%s\"\n", kusername);
  DEEDEBUG2("pname is \"%s\"\n", pname);
  if (strcmp(kusername, pname)) {
   DEEDEBUG("Principals not equal\n");
   goto return1;
  }
  DEEDEBUG("Principals equal\n");

  realm = strchr(pname,'@');
  realm++;

  if ((tgtname = malloc(9 + 2 * strlen(realm))) == 0) {
       fprintf(stderr,"Malloc failed for tgtname\n");
       goto return1;
  }

  strcpy(tgtname,"krbtgt/");
  strcat(tgtname,realm);
  strcat(tgtname,"@");
  strcat(tgtname,realm);
 
  DEEDEBUG2("Getting tgt %s\n", tgtname);
  if (code = krb5_cc_start_seq_get(*pcache, &cur)) {
    com_err(progname, code, "while starting to retrieve tickets");
    goto return1;
  }

  while (!(code = krb5_cc_next_cred(*pcache, &cur, &creds))) {
    krb5_creds *cred = &creds;

    if (code = krb5_unparse_name(cred->server, &sname)) {
      com_err(progname, code, "while unparsing server name");
      continue;
    }

    if (strncmp(sname, tgtname, strlen(tgtname)) == 0) {
      DEEDEBUG("FOUND\n");
      if (code = krb5_copy_creds(&creds, tgt)) {
        com_err(progname, code, "while copying TGT");
        goto return1;
      }
      found = 0;
      break;
    } 
    /* we should do a krb5_free_cred_contents(creds); */
  }

  if (code = krb5_cc_end_seq_get(*pcache, &cur)) {
    com_err(progname, code, "while finishing retrieval"); 
    goto return2;
  }

return1:
  flags = KRB5_TC_OPENCLOSE; 
  krb5_cc_set_flags(*pcache, flags); /* force a close */
   
return2:
  if (tgtname)
    free(tgtname);

  return(found);
}


/*------------------------------------------*/
/* Convert a forwarded TGT to a DCE context */
int k5dcecon(luid, luser, pname)
  uid_t luid;
  char *luser;
  char *pname;
{

  krb5_creds *ftgt = NULL;
  krb5_creds *tgt = NULL;
  unsigned32 dfspag;
  boolean32 reset_passwd = 0;
  int lst;
  dce_error_string_t  err_string;
  char *shell_prog;
  krb5_ccache fcache;
  char *ccname;
  char *kusername;
  char *urealm;
  char *cp;
  int pag;
  int code;
  krb5_timestamp endtime;


  /* If there is no cache to be converted, we should not be here */

  if ((ccname = getenv("KRB5CCNAME")) == NULL) {
    DEEDEBUG("No KRB5CCNAME\n");
    return(1);
  }

  if (k5dcegettgt(&fcache, ccname, pname, &ftgt)) {
    fprintf(stderr, "%s: Did not find TGT\n", progname);
    return(1);
  }

 
  DEEDEBUG2("flags=%x\n",ftgt->ticket_flags);
  if (!(ftgt->ticket_flags & TKT_FLG_FORWARDABLE)){
    fprintf(stderr,"Ticket not forwardable\n");
    return(0); /* but OK to continue */
  }

  setenv("KRB5CCNAME","",1);
    
#define TKT_ACCEPTABLE (TKT_FLG_FORWARDABLE | TKT_FLG_PROXIABLE \
         | TKT_FLG_MAY_POSTDATE | TKT_FLG_RENEWABLE | TKT_FLG_HW_AUTH \
         | TKT_FLG_PRE_AUTH)

  if (!k5dcesession(luid, pname, &tgt, &pag, 
        (ftgt->ticket_flags & TKT_ACCEPTABLE))) {
    if (ftgt->times.endtime > tgt->times.endtime) {
      DEEDEBUG("Updating existing cache\n"); 
      return(k5dceupdate(&ftgt, pag));
    } else {
      DEEDEBUG("Using existing cache\n");
      return(0); /* use the original one */
    }
  } 
    /* see if the tgts match up */

  if ((code = k5dcecreate(luid, luser, pname, &ftgt))) {
	return (code);
  }

  /*
   * Destroy the Kerberos5 cred cache file.
   * but dont care aout the return code. 
   */

  DEEDEBUG("Destroying the old cache\n");
  if ((code = krb5_cc_destroy(fcache))) {
    com_err(progname, code, "while destroying Kerberos5 ccache");
  }
  return (0);
}


/*--------------------------------------------------*/
/* k5dceupdate - update the cache with a new TGT    */
/* Assumed that the KRB5CCNAME has been set         */

int k5dceupdate(krbtgt, pag) 
   krb5_creds **krbtgt;
   int pag;
{
 
  krb5_ccache ccache;
  int code;

  if (code = krb5_cc_default(&ccache)) {
    com_err(progname, code, "while opening cache for update");
    return(2);
   }

  if (code = ccache->ops->init(ccache,(*krbtgt)->client)) {
    com_err(progname, code, "while reinitilizing cache");
    return(3);
  } 

    /* krb5_cc_store_cred */
  if (code = ccache->ops->store(ccache, *krbtgt)) {
    com_err(progname, code, "while updating cache");
    return(2);
  }

  sec_login_pag_new_tgt(pag, (*krbtgt)->times.endtime);
  return(0);
}
/*--------------------------------------------------*/
/* k5dcecreate - create a new DCE context           */

int k5dcecreate(luid, luser, pname, krbtgt)
   uid_t luid;
   char *luser;
   char *pname;
   krb5_creds **krbtgt;
{
   
    char *cp;
    char *urealm;
    char *username;
    char *defrealm;
    uid_t uid;

    error_status_t st;
    sec_login_handle_t lcontext = 0;
    sec_login_auth_src_t auth_src = 0;
    boolean32 reset_passwd = 0;
    int lst;
    dce_error_string_t  err_string;

	setenv("KRB5CCNAME","",1); /* make sure it not misused */

	uid = getuid();
	DEEDEBUG2("uid=%d\n",uid);
    
	/* if run as root, change to user, so as to have the
	 * cache created for the local user even if cross-cell
	 * If run as a user, let standard file protection work.
	 */

	if (uid == 0) {
		if (seteuid(luid) < 0)
			goto abort;
	}  

	cp = strchr(pname,'@');
	*cp = '\0';
	urealm = ++cp;

 DEEDEBUG2("basename=%s\n",cp);
 DEEDEBUG2("realm=%s\n",urealm);

    /* now build the username as a single string or a /.../cell/user
     * if this is a cross cell
     */

	if ((username = malloc(7+strlen(pname)+strlen(urealm))) == 0) {
         fprintf(stderr,"Malloc failed for username\n");
         goto abort;
    }
    if (krb5_get_default_realm(&defrealm)) {
        DEEDEBUG("krb5_get_default_realm failed\n");
        goto abort;
    }


    if (!strcmp(urealm,defrealm)) {
        strcpy(username,pname);
    } else {
        strcpy(username,"/.../");
        strcat(username,urealm);
        strcat(username,"/");
        strcat(username,pname);
    }

    /*
     * Setup a DCE login context
     */

    if (sec_login_setup_identity((unsigned_char_p_t)username, 
				 (sec_login_external_tgt|sec_login_proxy_cred),
				 &lcontext, &st)) {
	/*
	 * Add our TGT.
	 */
	  DEEDEBUG("Adding our new TGT\n");
	  sec_login_krb5_add_cred(lcontext, *krbtgt, &st);
	  if (st) {
	    dce_error_inq_text(st, err_string, &lst);
	    fprintf(stderr,
				"Error while adding credentials for %s because %s\n", 
				username, err_string);
	    goto abort;
	  }	
	  DEEDEBUG("validating and certifying\n");
	  /*
	   * Now "validate" and certify the identity,
	   *  usually we would pass a password here, but...
	   * sec_login_valid_and_cert_ident
	   * sec_login_validate_identity
	   */

	  if (sec_login_validate_identity(lcontext, 0, &reset_passwd,
		 &auth_src, &st)) {
	    DEEDEBUG2("validate_identity st=%d\n",st);
	    if (st) {
		  dce_error_inq_text(st, err_string, &lst);
		  fprintf(stderr, "Validation error for %s because %s\n",
				 username, err_string);
		  goto abort;
	    }
		if (!sec_login_certify_identity(lcontext,&st)) {
			dce_error_inq_text(st, err_string, &lst);
			fprintf(stderr,
			"Credentials not certified because %s\n",err_string);
		}
	    if (reset_passwd) {
		 fprintf(stderr,
                "Password must be changed for %s\n", username);
	    }
	    if (auth_src == sec_login_auth_src_local) {
		fprintf(stderr,
			 "Credentials obtained from local registry for %s\n", 
			 username);
	    }
	    if (auth_src == sec_login_auth_src_overridden) {
		  fprintf(stderr, "Validated %s from local override entry, no network credentials obtained\n", username);
		  goto abort; 

	    }
	    /*
	     * Actually create the cred files.
	     */
		DEEDEBUG("Ceating new cred files.\n");
	    sec_login_set_context(lcontext, &st);
	    if (st) {
		  dce_error_inq_text(st, err_string, &lst);
		  fprintf(stderr, 
                "Unable to set context for %s because %s\n",
		    username, err_string);
		  goto abort;
	    }

        /*
         * Now free up the local context and leave the 
         * network context with its pag
         */
#if 0
        sec_login_release_context(&lcontext, &st);
        if (st) {
          dce_error_inq_text(st, err_string, &lst);
          fprintf(stderr,
               "Unable to release context for %s because %s\n",
            username, err_string);
          goto abort;
        }
#endif
	}
	else {
	  DEEDEBUG2("validate failed %d\n",st);
	  dce_error_inq_text(st, err_string, &lst);
	  fprintf(stderr,
             "Unable to validate %s because %s\n", username, 
			err_string);
	  goto abort;
	}
  }
  else {
	dce_error_inq_text(st, err_string, &lst);
	fprintf(stderr, 
          "Unable to setup login entry for %s because %s\n",
       username, err_string);
	  goto abort;
  }

 done:
    /* if we were root, get back to root */

    DEEDEBUG2("sec_login_inq_pag %8.8x\n",
             sec_login_inq_pag(lcontext, &st));

    if (uid == 0) {
      seteuid(0);
    }  

	DEEDEBUG("completed\n");
	return(0);

 abort:
    if (uid == 0) {
      seteuid(0);
    }  

    DEEDEBUG("Aborting\n");
    return(2);
}



/*-------------------------------------------------*/
main(argc, argv)
  int argc;
  char *argv[];
{
  int status;
  extern int optind;
  extern char *optarg;
  int rv;

  char *lusername = NULL;
  char *pname = NULL;
  int fflag = 0;
  struct passwd *pw;
  uid_t luid;
  uid_t myuid;
  char *ccname;
  krb5_creds *tgt = NULL;

#ifdef DEBUG
  close(2);
  open("/tmp/k5dce.debug",O_WRONLY|O_CREAT|O_APPEND, 0600);
#endif

  if (myuid = getuid()) {
    DEEDEBUG2("UID = %d\n",myuid);
    exit(33); /* must be root to run this, get out now */
  }

  while ((rv = getopt(argc,argv,"l:p:fs")) != -1) {
    DEEDEBUG2("Arg = %c\n", rv);
    switch(rv) {
      case 'l':         /* user name */
	lusername = optarg;
	DEEDEBUG2("Optarg = %s\n", optarg);
	break;
      case 'p':         /* principal name */
        pname = optarg; 
	DEEDEBUG2("Optarg = %s\n", optarg);
        break;
      case 'f':         /* convert a forwarded TGT to a context */
        fflag++;
        break;
      case 's':      /* old test parameter, ignore it */
        break; 
    }
  }

  setlocale(LC_ALL, "");
  krb5_init_ets();
  time(&now); /* set time to check expired tickets */

  /* if lusername == NULL, Then user is passed as the USER= variable */ 

  if (!lusername) {
    lusername = getenv("USER");
    if (!lusername) {
      fprintf(stderr, "USER not in environment\n");
      return(3);
    }
  }

  if ((pw = getpwnam(lusername)) == NULL) {
    fprintf(stderr, "Who are you?\n");
    return(44);
  }

  luid = pw->pw_uid;

  if (fflag) {  
    status = k5dcecon(luid, lusername, pname); 
  } else {
    status = k5dcesession(luid, pname, &tgt, NULL, 0);
  }
 
  if (!status) {
    printf("%s",getenv("KRB5CCNAME")); /* return via stdout to caller */
    DEEDEBUG2("KRB5CCNAME=%s\n",getenv("KRB5CCNAME"));
  }

  DEEDEBUG2("Returning status %d\n",status);
  return (status);
}
