/*
 *  krb5-agent - Renews Kerberos tickets periodically.
 *  Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com)
 *  
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <krb5.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

volatile int died = 0;
pid_t child;
int execmode = 0, failsafe = 0;
int verbose = 0, quiet = 0;
time_t renewat;

krb5_context context = NULL;
krb5_ccache ccache = NULL;

void cleanup_krb5(void)
{
    if(ccache != NULL)
	krb5_cc_close(context, ccache);
    ccache = NULL;
    if(context != NULL)
	krb5_free_context(context);
    context = NULL;
}

void sighandler(int sig)
{
    switch(sig) {
    case SIGCHLD:
	died = 1;
	break;
    case SIGHUP:
    case SIGINT:
    case SIGTERM:
	if(execmode)
	    kill(child, sig);
	break;
    }
}

void renew(void)
{
    int ret;
    krb5_principal me;
    krb5_creds creds;
    
    if(context == NULL)
	return;
    if(ccache == NULL) {
	if((ret = krb5_cc_default(context, &ccache)) != 0) {
	    if(!quiet)
		fprintf(stderr, "could not initialize Kerberos context: %s\n", error_message(ret));
	    return;
	}
    }
    
    me = NULL;
    memset(&creds, 0, sizeof(creds));
    if((ret = krb5_cc_get_principal(context, ccache, &me)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not get principal from cache: %s\n", error_message(ret));
	goto out;
    }
    if((ret = krb5_get_renewed_creds(context, &creds, me, ccache, NULL)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not get renewed credentials: %s\n", error_message(ret));
	goto out;
    }
    if((ret = krb5_cc_initialize(context, ccache, me)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not reinitialize cache: %s\n", error_message(ret));
	krb5_free_cred_contents(context, &creds);
	goto out;
    }
    if((ret = krb5_cc_store_cred(context, ccache, &creds)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not store renewed credentials: %s\n", error_message(ret));
	krb5_free_cred_contents(context, &creds);
	goto out;
    }
    krb5_free_cred_contents(context, &creds);
    if(verbose >= 1)
	printf("successfully renewed credentials\n");
    
 out:
    if(me != NULL)
	krb5_free_principal(context, me);
}

time_t goodrenewtime(void)
{
    int ret;
    krb5_principal me;
    krb5_cc_cursor cur;
    krb5_creds creds;
    time_t now, good;
    
    if(context == NULL)
	return(-1);
    if(ccache == NULL) {
	if((ret = krb5_cc_default(context, &ccache)) != 0) {
	    if(!quiet)
		fprintf(stderr, "could not initialize Kerberos context: %s\n", error_message(ret));
	    return(-1);
	}
    }
    
    me = NULL;
    cur = NULL;
    good = -1;
    if((ret = krb5_cc_get_principal(context, ccache, &me)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not get principal from cache: %s\n", error_message(ret));
	goto out;
    }
    if((ret = krb5_cc_start_seq_get(context, ccache, &cur)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not open credentials cache: %s\n", error_message(ret));
	goto out;
    }
    now = time(NULL);
    while(!krb5_cc_next_cred(context, ccache, &cur, &creds)) {
	if(!strcmp(krb5_princ_component(context, creds.server, 0)->data, KRB5_TGS_NAME) &&
	   !strcmp(krb5_princ_component(context, creds.server, 1)->data, me->realm.data)) {
	    if(!creds.times.starttime)
		creds.times.starttime = creds.times.authtime;
	    good = (creds.times.starttime + (((creds.times.endtime - creds.times.starttime) * 9) / 10));
	    break;
	}
	krb5_free_cred_contents(context, &creds);
    }
    
 out:
    if(cur != NULL)
	krb5_cc_end_seq_get(context, ccache, &cur);
    if(me != NULL)
	krb5_free_principal(context, me);
    return(good);
}

int main(int argc, char **argv)
{
    char *p;
    int c;
    time_t interval, now;
    pid_t wpid;
    int ret, status;
    
    interval = -1;
    while((c = getopt(argc, argv, "+hi:vqf")) != -1) {
	switch(c) {
	case 'v':
	    verbose++;
	    break;
	case 'q':
	    quiet = 1;
	    break;
	case 'f':
	    failsafe = 1;
	    break;
	case 'i':
	    p = optarg + strlen(optarg) - 1;
	    if((*p >= 'a') && (*p <= 'z')) {
		if(*p == 'm')
		    interval = 60;
		else if(*p == 'h')
		    interval = 3600;
		else if(*p == 'd')
		    interval = 86400;
		else
		    interval = 1;
		*p = 0;
	    } else {
		interval = 1;
	    }
	    interval *= atoi(optarg);
	    break;
	case 'h':
	case '?':
	case ':':
	default:
	    fprintf(stderr, "usage: krb5-agent [-hvqf] [-i interval] [program args...]\n");
	    exit((c == 'h')?0:1);
	}
    }
    
    atexit(cleanup_krb5);
    if((ret = krb5_init_context(&context)) != 0) {
	if(!quiet)
	    fprintf(stderr, "could not initialize Kerberos context: %s\n", error_message(ret));
	if(!failsafe)
	    exit(1);
    }
    if(context != NULL) {
	if((ret = krb5_cc_default(context, &ccache)) != 0) {
	    if(!quiet)
		fprintf(stderr, "could not initialize Kerberos context: %s\n", error_message(ret));
	    if(!failsafe)
		exit(1);
	}
    }
    
    if(optind < argc) {
	execmode = 1;
	signal(SIGCHLD, sighandler);
	if((child = fork()) < 0) {
	    perror("fork");
	    exit(1);
	}
	if(child == 0) {
	    char buf[80];
	    snprintf(buf, 80, "KRB5_AGENT_PID=%i", getppid());
	    putenv(buf);
	    execvp(argv[optind], argv + optind);
	    perror(argv[optind]);
	    exit(255);
	}
	signal(SIGHUP, sighandler);
	signal(SIGINT, sighandler);
	signal(SIGTERM, sighandler);
    }
    now = time(NULL);
    if(interval >= 0)
	renewat = now + interval;
    while(1) {
	if(died) {
	    wpid = waitpid(-1, &status, WNOHANG);
	    if(wpid < 0) {
		perror("waitpid");
	    } else if(execmode && (wpid == child)) {
		/* Try to preserve exit status as best as we can... */
		if(WIFEXITED(status)) {
		    exit(WEXITSTATUS(status));
		} else {
		    cleanup_krb5();
		    signal(WTERMSIG(status), SIG_DFL);
		    kill(getpid(), WTERMSIG(status));
		    exit(255);
		}
	    }
	    died = 0;
	}
	if(interval < 0) {
	    if((renewat = goodrenewtime()) < 0) {
		renewat = -1;
	    } else if(verbose >= 2) {
		printf("will renew tickets at %s", ctime(&renewat));
	    }
	}
	if(renewat < 0)
	    sleep(60);
	else
	    sleep(renewat - now);
	now = time(NULL);
	if((renewat >= 0) && (now >= renewat)) {
	    renew();
	    now = time(NULL);
	    if(interval >= 0)
		renewat = now + interval;
	}
    }
}

/*
 * Local Variables:
 * compile-command: "gcc -Wall -g -o krb5-agent krb5-agent.c -lkrb5"
 * End:
 */

