/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 1993 OpenVision Technologies, Inc., All Rights Reserved
 *
 */

/*
 * Copyright (C) 1998 by the FundsXpress, INC.
 *
 * All rights reserved.
 *
 * Export of this software from the United States of America may require
 * a specific license from the United States Government.  It is the
 * responsibility of any person or organization contemplating export to
 * obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of FundsXpress. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  FundsXpress makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include <k5-platform.h>
#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <signal.h>
#include <syslog.h>
#include <sys/types.h>
#ifdef _AIX
#include <sys/select.h>
#endif
#include <sys/time.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netdb.h>
#include <gssrpc/rpc.h>
#include <gssapi/gssapi.h>
#include "gssapiP_krb5.h" /* for kg_get_context */
#include <gssrpc/auth_gssapi.h>
#include <kadm5/admin.h>
#include <kadm5/kadm_rpc.h>
#include <adm_proto.h>
#include "kdb_kt.h"  /* for krb5_ktkdb_set_context */
#include <string.h>
#include <kdb_log.h>

#include "misc.h"
#include "auth.h"

#if defined(NEED_DAEMON_PROTO)
int daemon(int, int);
#endif

#define TIMEOUT 15

gss_name_t gss_changepw_name = NULL, gss_oldchangepw_name = NULL;
void *global_server_handle;
int nofork = 0;
char *kdb5_util = KPROPD_DEFAULT_KDB5_UTIL;
char *kprop = KPROPD_DEFAULT_KPROP;
char *dump_file = KPROP_DEFAULT_FILE;
char *kprop_port = NULL;

static krb5_context context;
static char *progname;

static void
usage()
{
    fprintf(stderr, _("Usage: kadmind [-x db_args]* [-r realm] [-m] [-nofork] "
                      "[-port port-number]\n"
                      "\t\t[-proponly] [-p path-to-kdb5_util] [-F dump-file]\n"
                      "\t\t[-K path-to-kprop] [-k kprop-port] [-P pid_file]\n"
                      "\nwhere,\n\t[-x db_args]* - any number of database "
                      "specific arguments.\n"
                      "\t\t\tLook at each database documentation for "
                      "supported arguments\n"));
    exit(1);
}

/*
 * Output a message to stderr and the admin server log, and exit with status 1.
 * msg should not be punctuated.  If code is given, msg should indicate what
 * operation was taking place in the present progressive.  Otherwise msg should
 * be capitalized and should indicate what went wrong.
 */
static void
fail_to_start(krb5_error_code code, const char *msg)
{
    const char *errmsg;

    if (code) {
        errmsg = krb5_get_error_message(context, code);
        fprintf(stderr, _("%s: %s while %s, aborting\n"), progname, errmsg,
                msg);
        krb5_klog_syslog(LOG_ERR, _("%s while %s, aborting\n"), errmsg, msg);
    } else {
        fprintf(stderr, _("%s: %s, aborting\n"), progname, msg);
        krb5_klog_syslog(LOG_ERR, _("%s, aborting"), msg);
    }
    exit(1);
}

static int
write_pid_file(const char *pid_file)
{
    FILE *file;
    unsigned long pid;
    int st1, st2;

    file = fopen(pid_file, "w");
    if (file == NULL)
        return errno;
    pid = (unsigned long)getpid();
    st1 = (fprintf(file, "%ld\n", pid) < 0) ? errno : 0;
    st2 = (fclose(file) == EOF) ? errno : 0;
    return st1 ? st1 : st2;
}

/* Set up the main loop.  If proponly is set, don't set up ports for kpasswd or
 * kadmin.  May set *ctx_out even on error. */
static krb5_error_code
setup_loop(kadm5_config_params *params, int proponly, verto_ctx **ctx_out)
{
    krb5_error_code ret;
    verto_ctx *ctx;

    *ctx_out = ctx = loop_init(VERTO_EV_TYPE_SIGNAL);
    if (ctx == NULL)
        return ENOMEM;
    ret = loop_setup_signals(ctx, &global_server_handle, NULL);
    if (ret)
        return ret;
    if (!proponly) {
        ret = loop_add_udp_address(params->kpasswd_port,
                                   params->kpasswd_listen);
        if (ret)
            return ret;
        ret = loop_add_tcp_address(params->kpasswd_port,
                                   params->kpasswd_listen);
        if (ret)
            return ret;
        ret = loop_add_rpc_service(params->kadmind_port,
                                   params->kadmind_listen,
                                   KADM, KADMVERS, kadm_1);
        if (ret)
            return ret;
    }
#ifndef DISABLE_IPROP
    if (params->iprop_enabled) {
        ret = loop_add_rpc_service(params->iprop_port, params->iprop_listen,
                                   KRB5_IPROP_PROG, KRB5_IPROP_VERS,
                                   krb5_iprop_prog_1);
        if (ret)
            return ret;
    }
#endif
    return loop_setup_network(ctx, &global_server_handle, progname,
                              DEFAULT_TCP_LISTEN_BACKLOG);
}

/* Point GSSAPI at the KDB keytab so we don't need an actual file keytab. */
static krb5_error_code
setup_kdb_keytab()
{
    krb5_error_code ret;

    ret = krb5_ktkdb_set_context(context);
    if (ret)
        return ret;
    ret = krb5_db_register_keytab(context);
    if (ret)
        return ret;
    return krb5_gss_register_acceptor_identity("KDB:");
}


/* Return "name@realm". */
static char *
build_princ_name(char *name, char *realm)
{
    char *fullname;

    if (asprintf(&fullname, "%s@%s", name, realm) < 0)
        return NULL;
    return fullname;
}

/* Callback from GSSRPC for garbled/forged/replayed/etc messages. */
static void
log_badverf(gss_name_t client_name, gss_name_t server_name,
            struct svc_req *rqst, struct rpc_msg *msg, char *data)
{
    static const struct {
        rpcproc_t proc;
        const char *proc_name;
    } proc_names[] = {
        {1, "CREATE_PRINCIPAL"},
        {2, "DELETE_PRINCIPAL"},
        {3, "MODIFY_PRINCIPAL"},
        {4, "RENAME_PRINCIPAL"},
        {5, "GET_PRINCIPAL"},
        {6, "CHPASS_PRINCIPAL"},
        {7, "CHRAND_PRINCIPAL"},
        {8, "CREATE_POLICY"},
        {9, "DELETE_POLICY"},
        {10, "MODIFY_POLICY"},
        {11, "GET_POLICY"},
        {12, "GET_PRIVS"},
        {13, "INIT"},
        {14, "GET_PRINCS"},
        {15, "GET_POLS"},
        {16, "SETKEY_PRINCIPAL"},
        /* 17 was "SETV4KEY_PRINCIPAL" */
        {18, "CREATE_PRINCIPAL3"},
        {19, "CHPASS_PRINCIPAL3"},
        {20, "CHRAND_PRINCIPAL3"},
        {21, "SETKEY_PRINCIPAL3"},
        {22, "PURGEKEYS"},
        {23, "GET_STRINGS"},
        {24, "SET_STRING"}
    };
    OM_uint32 minor;
    gss_buffer_desc client, server;
    gss_OID gss_type;
    const char *a;
    rpcproc_t proc;
    unsigned int i;
    const char *procname;
    size_t clen, slen;
    char *cdots, *sdots;

    client.length = 0;
    client.value = NULL;
    server.length = 0;
    server.value = NULL;

    (void)gss_display_name(&minor, client_name, &client, &gss_type);
    (void)gss_display_name(&minor, server_name, &server, &gss_type);
    if (client.value == NULL) {
        client.value = "(null)";
        clen = sizeof("(null)") - 1;
    } else {
        clen = client.length;
    }
    trunc_name(&clen, &cdots);
    if (server.value == NULL) {
        server.value = "(null)";
        slen = sizeof("(null)") - 1;
    } else {
        slen = server.length;
    }
    trunc_name(&slen, &sdots);
    a = client_addr(rqst->rq_xprt);

    proc = msg->rm_call.cb_proc;
    procname = NULL;
    for (i = 0; i < sizeof(proc_names) / sizeof(*proc_names); i++) {
        if (proc_names[i].proc == proc) {
            procname = proc_names[i].proc_name;
            break;
        }
    }
    if (procname != NULL) {
        krb5_klog_syslog(LOG_NOTICE,
                         _("WARNING! Forged/garbled request: %s, claimed "
                           "client = %.*s%s, server = %.*s%s, addr = %s"),
                         procname, (int)clen, (char *)client.value, cdots,
                         (int)slen, (char *)server.value, sdots, a);
    } else {
        krb5_klog_syslog(LOG_NOTICE,
                         _("WARNING! Forged/garbled request: %d, claimed "
                           "client = %.*s%s, server = %.*s%s, addr = %s"),
                         proc, (int)clen, (char *)client.value, cdots,
                         (int)slen, (char *)server.value, sdots, a);
    }

    (void)gss_release_buffer(&minor, &client);
    (void)gss_release_buffer(&minor, &server);
}

/* Callback from GSSRPC for miscellaneous errors */
static void
log_miscerr(struct svc_req *rqst, struct rpc_msg *msg, char *error, char *data)
{
    krb5_klog_syslog(LOG_NOTICE, _("Miscellaneous RPC error: %s, %s"),
                     client_addr(rqst->rq_xprt), error);
}

static void
log_badauth_display_status_1(char *m, OM_uint32 code, int type)
{
    OM_uint32 gssstat, minor_stat;
    gss_buffer_desc msg;
    OM_uint32 msg_ctx;

    msg_ctx = 0;
    while (1) {
        gssstat = gss_display_status(&minor_stat, code, type, GSS_C_NULL_OID,
                                     &msg_ctx, &msg);
        if (gssstat != GSS_S_COMPLETE) {
            krb5_klog_syslog(LOG_ERR, _("%s Cannot decode status %d"), m,
                             (int)code);
            return;
        }

        krb5_klog_syslog(LOG_NOTICE, "%s %.*s", m, (int)msg.length,
                         (char *)msg.value);
        (void)gss_release_buffer(&minor_stat, &msg);

        if (!msg_ctx)
            break;
    }
}

/* Callback from GSSRPC for authentication failures */
void
log_badauth(OM_uint32 major, OM_uint32 minor, SVCXPRT *xprt, char *data)
{
    krb5_klog_syslog(LOG_NOTICE, _("Authentication attempt failed: %s, "
                                   "GSS-API error strings are:"),
                     client_addr(xprt));
    log_badauth_display_status_1("   ", major, GSS_C_GSS_CODE);
    log_badauth_display_status_1("   ", minor, GSS_C_MECH_CODE);
    krb5_klog_syslog(LOG_NOTICE, _("   GSS-API error strings complete."));
}

int
main(int argc, char *argv[])
{
    OM_uint32 minor_status;
    gss_buffer_desc in_buf;
    gss_OID nt_krb5_name_oid = (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME;
    auth_gssapi_name names[4];
    kadm5_config_params params;
    verto_ctx *vctx;
    const char *pid_file = NULL;
    char **db_args = NULL, **tmpargs;
    const char *acl_file;
    int ret, i, db_args_size = 0, proponly = 0;

    setlocale(LC_ALL, "");
    setvbuf(stderr, NULL, _IONBF, 0);

    names[0].name = names[1].name = names[2].name = names[3].name = NULL;
    names[0].type = names[1].type = names[2].type = names[3].type =
        nt_krb5_name_oid;

    progname = (strrchr(argv[0], '/') != NULL) ? strrchr(argv[0], '/') + 1 :
        argv[0];

    memset(&params, 0, sizeof(params));

    argc--, argv++;
    while (argc) {
        if (strcmp(*argv, "-x") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            db_args_size++;
            tmpargs = realloc(db_args, sizeof(char *) * (db_args_size + 1));
            if (tmpargs == NULL) {
                fprintf(stderr, _("%s: cannot initialize. Not enough "
                                  "memory\n"), progname);
                exit(1);
            }
            db_args = tmpargs;
            db_args[db_args_size - 1] = *argv;
            db_args[db_args_size] = NULL;
        } else if (strcmp(*argv, "-r") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            params.realm = *argv;
            params.mask |= KADM5_CONFIG_REALM;
            argc--, argv++;
            continue;
        } else if (strcmp(*argv, "-m") == 0) {
            params.mkey_from_kbd = 1;
            params.mask |= KADM5_CONFIG_MKEY_FROM_KBD;
        } else if (strcmp(*argv, "-nofork") == 0) {
            nofork = 1;
#ifndef DISABLE_IPROP
        } else if (strcmp(*argv, "-proponly") == 0) {
            proponly = 1;
#endif
        } else if (strcmp(*argv, "-port") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            params.kadmind_port = atoi(*argv);
            params.mask |= KADM5_CONFIG_KADMIND_PORT;
        } else if (strcmp(*argv, "-P") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            pid_file = *argv;
        } else if (strcmp(*argv, "-W") == 0) {
            /* Ignore (deprecated weak random option). */
        } else if (strcmp(*argv, "-p") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            kdb5_util = *argv;
        } else if (strcmp(*argv, "-F") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            dump_file = *argv;
        } else if (strcmp(*argv, "-K") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            kprop = *argv;
        } else if (strcmp(*argv, "-k") == 0) {
            argc--, argv++;
            if (!argc)
                usage();
            kprop_port = *argv;
        } else {
            break;
        }
        argc--, argv++;
    }

    if (argc != 0)
        usage();

    ret = kadm5_init_krb5_context(&context);
    if (ret) {
        fprintf(stderr, _("%s: %s while initializing context, aborting\n"),
                progname, error_message(ret));
        exit(1);
    }

    krb5_klog_init(context, "admin_server", progname, 1);

    ret = kadm5_init(context, "kadmind", NULL, NULL, &params,
                     KADM5_STRUCT_VERSION, KADM5_API_VERSION_4, db_args,
                     &global_server_handle);
    if (ret)
        fail_to_start(ret, _("initializing"));

    ret = kadm5_get_config_params(context, 1, &params, &params);
    if (ret)
        fail_to_start(ret, _("getting config parameters"));
    if (!(params.mask & KADM5_CONFIG_REALM))
        fail_to_start(0, _("Missing required realm configuration"));
    if (!(params.mask & KADM5_CONFIG_ACL_FILE))
        fail_to_start(0, _("Missing required ACL file configuration"));
    if (proponly && !params.iprop_enabled) {
        fail_to_start(0, _("-proponly can only be used when "
                           "iprop_enable is true"));
    }

    ret = setup_loop(&params, proponly, &vctx);
    if (ret)
        fail_to_start(ret, _("initializing network"));

    names[0].name = build_princ_name(KADM5_ADMIN_SERVICE, params.realm);
    names[1].name = build_princ_name(KADM5_CHANGEPW_SERVICE, params.realm);
    if (names[0].name == NULL || names[1].name == NULL)
        fail_to_start(0, _("Cannot build GSSAPI auth names"));

    ret = setup_kdb_keytab();
    if (ret)
        fail_to_start(0, _("Cannot set up KDB keytab"));

    if (svcauth_gssapi_set_names(names, 2) == FALSE)
        fail_to_start(0, _("Cannot set GSSAPI authentication names"));

    /* if set_names succeeded, this will too */
    in_buf.value = names[1].name;
    in_buf.length = strlen(names[1].name) + 1;
    (void)gss_import_name(&minor_status, &in_buf, nt_krb5_name_oid,
                          &gss_changepw_name);

    svcauth_gssapi_set_log_badauth2_func(log_badauth, NULL);
    svcauth_gssapi_set_log_badverf_func(log_badverf, NULL);
    svcauth_gssapi_set_log_miscerr_func(log_miscerr, NULL);

    svcauth_gss_set_log_badauth2_func(log_badauth, NULL);
    svcauth_gss_set_log_badverf_func(log_badverf, NULL);
    svcauth_gss_set_log_miscerr_func(log_miscerr, NULL);

    if (svcauth_gss_set_svc_name(GSS_C_NO_NAME) != TRUE)
        fail_to_start(0, _("Cannot initialize GSSAPI service name"));

    acl_file = (*params.acl_file != '\0') ? params.acl_file : NULL;
    ret = auth_init(context, acl_file);
    if (ret)
        fail_to_start(ret, _("initializing ACL file"));

    /* Since some KDB modules are not fork-safe, we must reinitialize the
     * server handle after daemonizing. */
    kadm5_destroy(global_server_handle);
    global_server_handle = NULL;

    if (!nofork && daemon(0, 0) != 0)
        fail_to_start(errno, _("spawning daemon process"));
    if (pid_file != NULL) {
        ret = write_pid_file(pid_file);
        if (ret)
            fail_to_start(ret, _("creating PID file"));
    }

    ret = kadm5_init(context, "kadmind", NULL, NULL, &params,
                     KADM5_STRUCT_VERSION, KADM5_API_VERSION_4, db_args,
                     &global_server_handle);
    if (ret)
        fail_to_start(ret, _("initializing"));

    if (params.iprop_enabled == TRUE) {
        ulog_set_role(context, IPROP_PRIMARY);

        ret = ulog_map(context, params.iprop_logfile, params.iprop_ulogsize);
        if (ret)
            fail_to_start(ret, _("mapping update log"));

        if (nofork) {
            fprintf(stderr,
                    _("%s: create IPROP svc (PROG=%d, VERS=%d)\n"),
                    progname, KRB5_IPROP_PROG, KRB5_IPROP_VERS);
        }
    }

    if (kprop_port == NULL)
        kprop_port = getenv("KPROP_PORT");

    krb5_klog_syslog(LOG_INFO, _("starting"));
    if (nofork)
        fprintf(stderr, _("%s: starting...\n"), progname);

    verto_run(vctx);
    krb5_klog_syslog(LOG_INFO, _("finished, exiting"));

    /* Clean up memory, etc */
    svcauth_gssapi_unset_names();
    kadm5_destroy(global_server_handle);
    loop_free(vctx);
    auth_fini(context);
    (void)gss_release_name(&minor_status, &gss_changepw_name);
    (void)gss_release_name(&minor_status, &gss_oldchangepw_name);
    for (i = 0; i < 4; i++)
        free(names[i].name);

    krb5_klog_close(context);
    krb5_free_context(context);
    exit(0);
}
