/*
 * Copyright (c) 1997-2005 Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 * All rights reserved.
 *
 * Portions Copyright (c) 2009 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include "kdc_locl.h"
#include "send_to_kdc_plugin.h"

struct perf {
    unsigned long as_req;
    unsigned long tgs_req;
    struct timeval start;
    struct timeval stop;
    struct perf *next;
} *ptop;

int detach_from_console = -1;
int daemon_child = -1;
int do_bonjour = -1;

static krb5_kdc_configuration *kdc_config;
static krb5_context kdc_context;

static struct sockaddr_storage sa;
static const char *astr = "0.0.0.0";

static void eval_object(heim_object_t);


/*
 *
 */

static krb5_error_code
plugin_init(krb5_context context, void **pctx)
{
    *pctx = NULL;
    return 0;
}

static void
plugin_fini(void *ctx)
{
}

static krb5_error_code
plugin_send_to_kdc(krb5_context context,
		   void *ctx,
		   krb5_krbhst_info *ho,
		   time_t timeout,
		   const krb5_data *in,
		   krb5_data *out)
{
    return KRB5_PLUGIN_NO_HANDLE;
}

static krb5_error_code
plugin_send_to_realm(krb5_context context,
		     void *ctx,
		     krb5_const_realm realm,
		     time_t timeout,
		     const krb5_data *in,
		     krb5_data *out)
{
    int ret;

    krb5_kdc_update_time(NULL);

    ret = krb5_kdc_process_request(kdc_context, kdc_config,
				   in->data, in->length,
				   out, NULL, astr,
				   (struct sockaddr *)&sa, 0);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_kdc_process_request");

    return 0;
}

static krb5plugin_send_to_kdc_ftable send_to_kdc = {
    KRB5_PLUGIN_SEND_TO_KDC_VERSION_2,
    plugin_init,
    plugin_fini,
    plugin_send_to_kdc,
    plugin_send_to_realm
};

static void
perf_start(struct perf *perf)
{
    memset(perf, 0, sizeof(*perf));

    gettimeofday(&perf->start, NULL);
    perf->next = ptop;
    ptop = perf;
}

static void
perf_stop(struct perf *perf)
{
    gettimeofday(&perf->stop, NULL);
    ptop = perf->next;

    if (ptop) {
	ptop->as_req += perf->as_req;
	ptop->tgs_req += perf->tgs_req;
    }

    timevalsub(&perf->stop, &perf->start);
    printf("time: %lu.%06lu\n",
	   (unsigned long)perf->stop.tv_sec,
	   (unsigned long)perf->stop.tv_usec);

#define USEC_PER_SEC 1000000

    if (perf->as_req) {
	double as_ps = 0.0;
	as_ps = (perf->as_req * USEC_PER_SEC) / (double)((perf->stop.tv_sec * USEC_PER_SEC) + perf->stop.tv_usec);
	printf("as-req/s %.2lf  (total %lu requests)\n", as_ps, perf->as_req);
    }
	    
    if (perf->tgs_req) {
	double tgs_ps = 0.0;
	tgs_ps = (perf->tgs_req * USEC_PER_SEC) / (double)((perf->stop.tv_sec * USEC_PER_SEC) + perf->stop.tv_usec);
	printf("tgs-req/s %.2lf (total %lu requests)\n", tgs_ps, perf->tgs_req);
    }
}

/*
 *
 */

static void
eval_repeat(heim_dict_t o)
{
    heim_object_t or = heim_dict_get_value(o, HSTR("value"));
    heim_number_t n = heim_dict_get_value(o, HSTR("num"));
    int i, num;
    struct perf perf;

    perf_start(&perf);

    heim_assert(or != NULL, "value missing");
    heim_assert(n != NULL, "num missing");

    num = heim_number_get_int(n);
    heim_assert(num >= 0, "num >= 0");

    for (i = 0; i < num; i++)
	eval_object(or);

    perf_stop(&perf);
}

/*
 *
 */

static krb5_error_code
copy_keytab(krb5_context context, krb5_keytab from, krb5_keytab to)
{
    krb5_keytab_entry entry;
    krb5_kt_cursor cursor;
    krb5_error_code ret;

    ret = krb5_kt_start_seq_get(context, from, &cursor);
    if (ret)
	return ret;
    while ((ret = krb5_kt_next_entry(context, from, &entry, &cursor)) == 0){
	krb5_kt_add_entry(context, to, &entry);
	krb5_kt_free_entry(context, &entry);
    }
    (void) krb5_kt_end_seq_get(context, from, &cursor);
    if (ret == KRB5_KT_END)
	return 0;
    return ret;
}	    

/*
 *
 */

static void
eval_kinit(heim_dict_t o)
{
    heim_string_t user, password, keytab, fast_armor_cc, pk_user_id, ccache;
    krb5_get_init_creds_opt *opt;
    krb5_init_creds_context ctx;
    krb5_principal client;
    krb5_keytab ktmem = NULL;
    krb5_ccache fast_cc = NULL;
    krb5_error_code ret;

    if (ptop)
	ptop->as_req++;

    user = heim_dict_get_value(o, HSTR("client"));
    if (user == NULL)
	krb5_errx(kdc_context, 1, "no client");

    password = heim_dict_get_value(o, HSTR("password"));
    keytab = heim_dict_get_value(o, HSTR("keytab"));
    pk_user_id = heim_dict_get_value(o, HSTR("pkinit-user-cert-id"));
    if (password == NULL && keytab == NULL && pk_user_id == NULL)
	krb5_errx(kdc_context, 1, "password, keytab, nor PKINIT user cert ID");

    ccache = heim_dict_get_value(o, HSTR("ccache"));

    ret = krb5_parse_name(kdc_context, heim_string_get_utf8(user), &client);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_unparse_name");

    /* PKINIT parts */
    ret = krb5_get_init_creds_opt_alloc (kdc_context, &opt);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_alloc");

    if (pk_user_id) {
	heim_bool_t rsaobj = heim_dict_get_value(o, HSTR("pkinit-use-rsa"));
	int use_rsa = rsaobj ? heim_bool_val(rsaobj) : 0;

	ret = krb5_get_init_creds_opt_set_pkinit(kdc_context, opt,
						 client,
						 heim_string_get_utf8(pk_user_id),
						 NULL, NULL, NULL,
						 use_rsa ? 2 : 0,
						 NULL, NULL, NULL);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_set_pkinit");
    }

    fast_armor_cc = heim_dict_get_value(o, HSTR("fast-armor-cc"));
    if (fast_armor_cc) {

	ret = krb5_cc_resolve(kdc_context, heim_string_get_utf8(fast_armor_cc), &fast_cc);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_cc_resolve");

	ret = krb5_get_init_creds_opt_set_fast_ccache(kdc_context, opt, fast_cc);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_set_fast_ccache");

	ret = krb5_get_init_creds_opt_set_fast_flags(kdc_context, opt, KRB5_FAST_REQUIRED|KRB5_FAST_KDC_VERIFIED);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_set_fast_flags");

	fast_cc = NULL;
    }
    
    ret = krb5_init_creds_init(kdc_context, client, NULL, NULL, 0, opt, &ctx);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_init_creds_init");

    if (password) {
	ret = krb5_init_creds_set_password(kdc_context, ctx, 
					   heim_string_get_utf8(password));
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_init_creds_set_password");
    }
    if (keytab) {
	krb5_keytab kt = NULL;

	ret = krb5_kt_resolve(kdc_context, heim_string_get_utf8(keytab), &kt);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_kt_resolve");

	ret = krb5_kt_resolve(kdc_context, "MEMORY:keytab", &ktmem);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_kt_resolve(MEMORY)");

	ret = copy_keytab(kdc_context, kt, ktmem);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "copy_keytab");

	krb5_kt_close(kdc_context, kt);

	ret = krb5_init_creds_set_keytab(kdc_context, ctx, ktmem);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_init_creds_set_keytab");
    }

    ret = krb5_init_creds_get(kdc_context, ctx);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_init_creds_get");

    if (ccache) {
	const char *name = heim_string_get_utf8(ccache);
	krb5_creds cred;
	krb5_ccache cc;

	ret = krb5_init_creds_get_creds(kdc_context, ctx, &cred);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_init_creds_get_creds");

	ret = krb5_cc_resolve(kdc_context, name, &cc);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_cc_resolve");

	krb5_init_creds_store(kdc_context, ctx, cc);

	ret = krb5_cc_close(kdc_context, cc);
	if (ret)
	    krb5_err(kdc_context, 1, ret, "krb5_cc_close");

	krb5_free_cred_contents(kdc_context, &cred);
    }

    krb5_init_creds_free(kdc_context, ctx);

    if (ktmem)
	krb5_kt_close(kdc_context, ktmem);
    if (fast_cc)
	krb5_cc_close(kdc_context, fast_cc);
}

/*
 *
 */

static void
eval_kgetcred(heim_dict_t o)
{
    heim_string_t server, ccache;
    krb5_get_creds_opt opt;
    heim_bool_t nostore;
    krb5_error_code ret;
    krb5_ccache cc = NULL;
    krb5_principal s;
    krb5_creds *out = NULL;

    if (ptop)
	ptop->tgs_req++;

    server = heim_dict_get_value(o, HSTR("server"));
    if (server == NULL)
	krb5_errx(kdc_context, 1, "no server");

    ccache = heim_dict_get_value(o, HSTR("ccache"));
    if (ccache == NULL)
	krb5_errx(kdc_context, 1, "no ccache");

    nostore = heim_dict_get_value(o, HSTR("nostore"));
    if (nostore == NULL)
	nostore = heim_bool_create(1);

    ret = krb5_cc_resolve(kdc_context, heim_string_get_utf8(ccache), &cc);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_cc_resolve");

    ret = krb5_parse_name(kdc_context, heim_string_get_utf8(server), &s);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_parse_name");

    ret = krb5_get_creds_opt_alloc(kdc_context, &opt);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_get_creds_opt_alloc");

    if (heim_bool_val(nostore))
	krb5_get_creds_opt_add_options(kdc_context, opt, KRB5_GC_NO_STORE);

    ret = krb5_get_creds(kdc_context, opt, cc, s, &out);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_get_creds");
    
    krb5_free_creds(kdc_context, out);
    krb5_free_principal(kdc_context, s);
    krb5_get_creds_opt_free(kdc_context, opt);
    krb5_cc_close(kdc_context, cc);
}


/*
 *
 */

static void
eval_kdestroy(heim_dict_t o)
{
    heim_string_t ccache = heim_dict_get_value(o, HSTR("ccache"));;
    krb5_error_code ret;
    const char *name;
    krb5_ccache cc;

    heim_assert(ccache != NULL, "ccache_missing");
	
    name = heim_string_get_utf8(ccache);

    ret = krb5_cc_resolve(kdc_context, name, &cc);
    if (ret)
	krb5_err(kdc_context, 1, ret, "krb5_cc_resolve");

    krb5_cc_destroy(kdc_context, cc);
}


/*
 *
 */

static void
eval_array_element(heim_object_t o, void *ptr, int *stop)
{
    eval_object(o);
}

static void
eval_object(heim_object_t o)
{
    heim_tid_t t = heim_get_tid(o);

    if (t == heim_array_get_type_id()) {
	heim_array_iterate_f(o, NULL, eval_array_element);
    } else if (t == heim_dict_get_type_id()) {
	const char *op = heim_dict_get_value(o, HSTR("op"));

	heim_assert(op != NULL, "op missing");

	if (strcmp(op, "repeat") == 0) {
	    eval_repeat(o);
	} else if (strcmp(op, "kinit") == 0) {
	    eval_kinit(o);
	} else if (strcmp(op, "kgetcred") == 0) {
	    eval_kgetcred(o);
	} else if (strcmp(op, "kdestroy") == 0) {
	    eval_kdestroy(o);
	} else {
	    errx(1, "unsupported ops %s", op);
	}

    } else
	errx(1, "unsupported");
}


int
main(int argc, char **argv)
{
    krb5_error_code ret;
    int optidx = 0;

    setprogname(argv[0]);

    ret = krb5_init_context(&kdc_context);
    if (ret == KRB5_CONFIG_BADFORMAT)
	errx (1, "krb5_init_context failed to parse configuration file");
    else if (ret)
	errx (1, "krb5_init_context failed: %d", ret);

    ret = krb5_kt_register(kdc_context, &hdb_get_kt_ops);
    if (ret)
	errx (1, "krb5_kt_register(HDB) failed: %d", ret);

    kdc_config = configure(kdc_context, argc, argv, &optidx);

    argc -= optidx;
    argv += optidx;

    if (argc == 0)
	errx(1, "missing operations");

    krb5_plugin_register(kdc_context, PLUGIN_TYPE_DATA,
			 KRB5_PLUGIN_SEND_TO_KDC, &send_to_kdc);

    {
	void *buf;
	size_t size;
	heim_object_t o;

	if (rk_undumpdata(argv[0], &buf, &size))
	    errx(1, "undumpdata: %s", argv[0]);
	
	o = heim_json_create_with_bytes(buf, size, 10, 0, NULL);
	free(buf);
	if (o == NULL)
	    errx(1, "heim_json");
	
	/*
	 * do the work here
	 */
	
	eval_object(o);

	heim_release(o);
    }

    krb5_free_context(kdc_context);
    return 0;
}
