/*
 * Copyright (C) 1996-2024 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.
 */

/*
 * -----------------------------------------------------------------------------
 *
 * Author: Markus Moeller (markus_moeller at compuserve.com)
 *
 * Copyright (C) 2007 Markus Moeller. All rights reserved.
 *
 *   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.
 *
 *   As a special exemption, M Moeller gives permission to link this program
 *   with MIT, Heimdal or other GSS/Kerberos libraries, and distribute
 *   the resulting executable, without including the source code for
 *   the Libraries in the source distribution.
 *
 * -----------------------------------------------------------------------------
 */

#include "squid.h"
#include "rfc1738.h"

#if HAVE_GSSAPI

#include "negotiate_kerberos.h"

#if HAVE_SYS_STAT_H
#include "sys/stat.h"
#endif
#if HAVE_UNISTD_H
#include "unistd.h"
#endif

#if HAVE_KRB5_MEMORY_KEYTAB
typedef struct _krb5_kt_list {
    struct _krb5_kt_list *next;
    krb5_keytab_entry *entry;
} *krb5_kt_list;
krb5_kt_list ktlist = nullptr;

krb5_keytab memory_keytab;

krb5_error_code krb5_free_kt_list(krb5_context context, krb5_kt_list kt_list);
krb5_error_code krb5_write_keytab(krb5_context context,
                                  krb5_kt_list kt_list,
                                  char *name);
krb5_error_code krb5_read_keytab(krb5_context context,
                                 char *name,
                                 krb5_kt_list *kt_list);
#endif /* HAVE_KRB5_MEMORY_KEYTAB */

int
check_k5_err(krb5_context context, const char *function, krb5_error_code code)
{

    if (code && code != KRB5_KT_END) {
        const char *errmsg;
        errmsg = krb5_get_error_message(context, code);
        debug((char *) "%s| %s: ERROR: %s failed: %s\n", LogTime(), PROGRAM, function, errmsg);
        fprintf(stderr, "%s| %s: ERROR: %s: %s\n", LogTime(), PROGRAM, function, errmsg);
#if HAVE_KRB5_FREE_ERROR_MESSAGE
        krb5_free_error_message(context, errmsg);
#elif HAVE_KRB5_FREE_ERROR_STRING
        krb5_free_error_string(context, (char *)errmsg);
#else
        xfree(errmsg);
#endif
    }
    return code;
}

char *
gethost_name(void)
{
    /*
     * char hostname[sysconf(_SC_HOST_NAME_MAX)];
     */
    char hostname[1024];
    struct addrinfo *hres = nullptr, *hres_list;
    int rc;

    rc = gethostname(hostname, sizeof(hostname)-1);
    if (rc) {
        debug((char *) "%s| %s: ERROR: resolving hostname '%s' failed\n", LogTime(), PROGRAM, hostname);
        fprintf(stderr, "%s| %s: ERROR: resolving hostname '%s' failed\n",
                LogTime(), PROGRAM, hostname);
        return nullptr;
    }
    rc = getaddrinfo(hostname, nullptr, nullptr, &hres);
    if (rc != 0 || hres == nullptr ) {
        debug((char *) "%s| %s: ERROR: resolving hostname with getaddrinfo: %s failed\n",
              LogTime(), PROGRAM, gai_strerror(rc));
        fprintf(stderr,
                "%s| %s: ERROR: resolving hostname with getaddrinfo: %s failed\n",
                LogTime(), PROGRAM, gai_strerror(rc));
        return nullptr;
    }
    hres_list = hres;
    while (hres_list) {
        hres_list = hres_list->ai_next;
    }
    rc = getnameinfo(hres->ai_addr, hres->ai_addrlen, hostname,
                     sizeof(hostname), nullptr, 0, 0);
    if (rc != 0) {
        debug((char *) "%s| %s: ERROR: resolving ip address with getnameinfo: %s failed\n",
              LogTime(), PROGRAM, gai_strerror(rc));
        fprintf(stderr,
                "%s| %s: ERROR: resolving ip address with getnameinfo: %s failed\n",
                LogTime(), PROGRAM, gai_strerror(rc));
        freeaddrinfo(hres);
        return nullptr;
    }
    freeaddrinfo(hres);
    hostname[sizeof(hostname)-1] = '\0';
    return (xstrdup(hostname));
}

int
check_gss_err(OM_uint32 major_status, OM_uint32 minor_status,
              const char *function, int log, int sout)
{
    if (GSS_ERROR(major_status)) {
        OM_uint32 maj_stat, min_stat;
        OM_uint32 msg_ctx = 0;
        gss_buffer_desc status_string;
        char buf[1024];
        size_t len;

        len = 0;
        msg_ctx = 0;
        do {
            /* convert major status code (GSS-API error) to text */
            maj_stat = gss_display_status(&min_stat, major_status,
                                          GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_ctx, &status_string);
            if (maj_stat == GSS_S_COMPLETE && status_string.length > 0) {
                if (sizeof(buf) > len + status_string.length + 1) {
                    snprintf(buf + len, (sizeof(buf) - len), "%s", (char *) status_string.value);
                    len += status_string.length;
                }
            } else
                msg_ctx = 0;
            gss_release_buffer(&min_stat, &status_string);
        } while (msg_ctx);
        if (sizeof(buf) > len + 2) {
            snprintf(buf + len, (sizeof(buf) - len), "%s", ". ");
            len += 2;
        }
        msg_ctx = 0;
        do {
            /* convert minor status code (underlying routine error) to text */
            maj_stat = gss_display_status(&min_stat, minor_status,
                                          GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_ctx, &status_string);
            if (maj_stat == GSS_S_COMPLETE && status_string.length > 0) {
                if (sizeof(buf) > len + status_string.length) {
                    snprintf(buf + len, (sizeof(buf) - len), "%s", (char *) status_string.value);
                    len += status_string.length;
                }
            } else
                msg_ctx = 0;
            gss_release_buffer(&min_stat, &status_string);
        } while (msg_ctx);
        debug((char *) "%s| %s: ERROR: %s failed: %s\n", LogTime(), PROGRAM, function, buf);
        if (sout)
            fprintf(stdout, "BH %s failed: %s\n", function, buf);
        if (log)
            fprintf(stderr, "%s| %s: INFO: User not authenticated\n", LogTime(),
                    PROGRAM);
        return (1);
    }
    return (0);
}

#if HAVE_KRB5_MEMORY_KEYTAB
/*
 * Free a kt_list
 */
krb5_error_code krb5_free_kt_list(krb5_context context, krb5_kt_list list)
{
    krb5_kt_list lp = list;

    while (lp) {
#if USE_HEIMDAL_KRB5 || ( HAVE_KRB5_KT_FREE_ENTRY && HAVE_DECL_KRB5_KT_FREE_ENTRY )
        krb5_error_code  retval = krb5_kt_free_entry(context, lp->entry);
#else
        krb5_error_code  retval = krb5_free_keytab_entry_contents(context, lp->entry);
#endif
        safe_free(lp->entry);
        if (check_k5_err(context, "krb5_kt_free_entry", retval))
            return retval;
        krb5_kt_list prev = lp;
        lp = lp->next;
        xfree(prev);
    }
    return 0;
}
/*
 * Read in a keytab and append it to list.  If list starts as NULL,
 * allocate a new one if necessary.
 */
krb5_error_code krb5_read_keytab(krb5_context context, char *name, krb5_kt_list *list)
{
    krb5_kt_list lp = nullptr, tail = nullptr, back = nullptr;
    krb5_keytab kt;
    krb5_keytab_entry *entry;
    krb5_kt_cursor cursor;
    krb5_error_code retval = 0;

    if (*list) {
        /* point lp at the tail of the list */
        for (lp = *list; lp->next; lp = lp->next);
        back = lp;
    }
    retval = krb5_kt_resolve(context, name, &kt);
    if (check_k5_err(context, "krb5_kt_resolve", retval))
        return retval;
    retval = krb5_kt_start_seq_get(context, kt, &cursor);
    if (check_k5_err(context, "krb5_kt_start_seq_get", retval))
        goto close_kt;
    for (;;) {
        entry = (krb5_keytab_entry *)xcalloc(1, sizeof (krb5_keytab_entry));
        if (!entry) {
            retval = ENOMEM;
            debug((char *) "%s| %s: ERROR: krb5_read_keytab failed: %s\n",
                  LogTime(), PROGRAM, strerror(retval));
            fprintf(stderr, "%s| %s: ERROR: krb5_read_keytab: %s\n",
                    LogTime(), PROGRAM, strerror(retval));
            break;
        }
        memset(entry, 0, sizeof (*entry));
        retval = krb5_kt_next_entry(context, kt, entry, &cursor);
        if (check_k5_err(context, "krb5_kt_next_entry", retval))
            break;

        if (!lp) {              /* if list is empty, start one */
            lp = (krb5_kt_list)xmalloc(sizeof (*lp));
            if (!lp) {
                retval = ENOMEM;
                debug((char *) "%s| %s: ERROR: krb5_read_keytab failed: %s\n",
                      LogTime(), PROGRAM, strerror(retval));
                fprintf(stderr, "%s| %s: ERROR: krb5_read_keytab: %s\n",
                        LogTime(), PROGRAM, strerror(retval));
                break;
            }
        } else {
            lp->next = (krb5_kt_list)xmalloc(sizeof (*lp));
            if (!lp->next) {
                retval = ENOMEM;
                debug((char *) "%s| %s: ERROR: krb5_read_keytab failed: %s\n",
                      LogTime(), PROGRAM, strerror(retval));
                fprintf(stderr, "%s| %s: ERROR: krb5_read_keytab: %s\n",
                        LogTime(), PROGRAM, strerror(retval));
                break;
            }
            lp = lp->next;
        }
        if (!tail)
            tail = lp;
        lp->next = nullptr;
        lp->entry = entry;
    }
    xfree(entry);
    if (retval) {
        if (retval == KRB5_KT_END)
            retval = 0;
        else {
            krb5_free_kt_list(context, tail);
            tail = nullptr;
            if (back)
                back->next = nullptr;
        }
    }
    if (!*list)
        *list = tail;
    krb5_kt_end_seq_get(context, kt, &cursor);
close_kt:
    krb5_kt_close(context, kt);
    return retval;
}

/*
 * Takes a kt_list and writes it to the named keytab.
 */
krb5_error_code krb5_write_keytab(krb5_context context, krb5_kt_list list, char *name)
{
    char ktname[MAXPATHLEN+sizeof("MEMORY:")+1];
    krb5_error_code retval = 0;

    snprintf(ktname, sizeof(ktname), "%s", name);
    retval = krb5_kt_resolve(context, ktname, &memory_keytab);
    if (retval)
        return retval;
    for (krb5_kt_list lp = list; lp; lp = lp->next) {
        retval = krb5_kt_add_entry(context, memory_keytab, lp->entry);
        if (retval)
            break;
    }
    /*
     *     krb5_kt_close(context, kt);
     */
    return retval;
}
#endif /* HAVE_KRB5_MEMORY_KEYTAB */

int
main(int argc, char *const argv[])
{
    char buf[MAX_AUTHTOKEN_LEN];
    char *c, *p;
    char *user = nullptr;
    char *rfc_user = nullptr;
#if HAVE_PAC_SUPPORT
    char ad_groups[MAX_PAC_GROUP_SIZE];
    char *ag=nullptr;
    krb5_pac pac;
#if USE_HEIMDAL_KRB5
    gss_buffer_desc data_set = GSS_C_EMPTY_BUFFER;
#else
    gss_buffer_desc type_id = GSS_C_EMPTY_BUFFER;
#endif
#endif
    krb5_context context = nullptr;
    krb5_error_code ret;
    long length = 0;
    static int err = 0;
    int opt, log = 0, norealm = 0;
    OM_uint32 ret_flags = 0, spnego_flag = 0;
    char *service_name = (char *) "HTTP", *host_name = nullptr;
    char *token = nullptr;
    char *service_principal = nullptr;
    char *keytab_name = nullptr;
    char *keytab_name_env = nullptr;
    char default_keytab[MAXPATHLEN] = {};
#if HAVE_KRB5_MEMORY_KEYTAB
    char *memory_keytab_name = nullptr;
    char *memory_keytab_name_env = nullptr;
#endif
    char *rcache_type = nullptr;
    char *rcache_type_env = nullptr;
    char *rcache_dir = nullptr;
    char *rcache_dir_env = nullptr;
    OM_uint32 major_status, minor_status;
    gss_ctx_id_t gss_context = GSS_C_NO_CONTEXT;
    gss_name_t client_name = GSS_C_NO_NAME;
    gss_name_t server_name = GSS_C_NO_NAME;
    gss_cred_id_t server_creds = GSS_C_NO_CREDENTIAL;
    gss_buffer_desc service = GSS_C_EMPTY_BUFFER;
    gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
    gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
    const unsigned char *kerberosToken = nullptr;
    const unsigned char *spnegoToken = nullptr;
    size_t spnegoTokenLength = 0;

    setbuf(stdout, nullptr);
    setbuf(stdin, nullptr);

    while (-1 != (opt = getopt(argc, argv, "dirs:k:c:t:"))) {
        switch (opt) {
        case 'd':
            debug_enabled = 1;
            break;
        case 'i':
            log = 1;
            break;
        case 'r':
            norealm = 1;
            break;
        case 'k':
#if HAVE_SYS_STAT_H
            struct stat fstat;
            char *ktp;
#endif
            if (optarg)
                keytab_name = xstrdup(optarg);
            else {
                fprintf(stderr, "ERROR: keytab file not given\n");
                exit(EXIT_FAILURE);
            }
            /*
             * Some sanity checks
             */
#if HAVE_SYS_STAT_H
            if ((ktp=strchr(keytab_name,':')))
                ktp++;
            else
                ktp=keytab_name;
            if (stat((const char*)ktp, &fstat)) {
                if (ENOENT == errno)
                    fprintf(stderr, "ERROR: keytab file %s does not exist\n",keytab_name);
                else
                    fprintf(stderr, "ERROR: Error %s during stat of keytab file %s\n",strerror(errno),keytab_name);
                exit(EXIT_FAILURE);
            } else if (!S_ISREG(fstat.st_mode)) {
                fprintf(stderr, "ERROR: keytab file %s is not a file\n",keytab_name);
                exit(EXIT_FAILURE);
            }
#endif
#if HAVE_UNISTD_H
            if (access(ktp, R_OK)) {
                fprintf(stderr, "ERROR: keytab file %s is not accessible\n",keytab_name);
                exit(EXIT_FAILURE);
            }
#endif
            break;
        case 'c':
#if HAVE_SYS_STAT_H
            struct stat dstat;
#endif
            if (optarg)
                rcache_dir = xstrdup(optarg);
            else {
                fprintf(stderr, "ERROR: replay cache directory not given\n");
                exit(EXIT_FAILURE);
            }
            /*
             * Some sanity checks
             */
#if HAVE_SYS_STAT_H
            if (stat((const char*)rcache_dir, &dstat)) {
                if (ENOENT == errno)
                    fprintf(stderr, "ERROR: replay cache directory %s does not exist\n",rcache_dir);
                else
                    fprintf(stderr, "ERROR: Error %s during stat of replay cache directory %s\n",strerror(errno),rcache_dir);
                exit(EXIT_FAILURE);
            } else if (!S_ISDIR(dstat.st_mode)) {
                fprintf(stderr, "ERROR: replay cache directory %s is not a directory\n",rcache_dir);
                exit(EXIT_FAILURE);
            }
#endif
#if HAVE_UNISTD_H
            if (access(rcache_dir, W_OK)) {
                fprintf(stderr, "ERROR: replay cache directory %s is not accessible\n",rcache_dir);
                exit(EXIT_FAILURE);
            }
#endif
            break;
        case 't':
            if (optarg)
                rcache_type = xstrdup(optarg);
            else {
                fprintf(stderr, "ERROR: replay cache type not given\n");
                exit(EXIT_FAILURE);
            }
            break;
        case 's':
            if (optarg)
                service_principal = xstrdup(optarg);
            else {
                fprintf(stderr, "ERROR: service principal not given\n");
                exit(EXIT_FAILURE);
            }
            break;
        default:
            fprintf(stderr, "Usage: \n");
            fprintf(stderr, "squid_kerb_auth [-d] [-i] [-s SPN] [-k keytab] [-c rcdir] [-t rctype]\n");
            fprintf(stderr, "-d full debug\n");
            fprintf(stderr, "-i informational messages\n");
            fprintf(stderr, "-r remove realm from username\n");
            fprintf(stderr, "-s service principal name\n");
            fprintf(stderr, "-k keytab name\n");
            fprintf(stderr, "-c replay cache directory\n");
            fprintf(stderr, "-t replay cache type\n");
            fprintf(stderr,
                    "The SPN can be set to GSS_C_NO_NAME to allow any entry from keytab\n");
            fprintf(stderr, "default SPN is HTTP/fqdn@DEFAULT_REALM\n");
            exit(EXIT_SUCCESS);
        }
    }

    debug((char *) "%s| %s: INFO: Starting version %s\n", LogTime(), PROGRAM, SQUID_KERB_AUTH_VERSION);
    if (service_principal && strcasecmp(service_principal, "GSS_C_NO_NAME")) {
        if (!strstr(service_principal,"HTTP/")) {
            debug((char *) "%s| %s: WARN: service_principal %s does not start with HTTP/\n",
                  LogTime(), PROGRAM, service_principal);
        }
        service.value = service_principal;
        service.length = strlen((char *) service.value);
    } else {
        host_name = gethost_name();
        if (!host_name) {
            fprintf(stderr,
                    "%s| %s: FATAL: Local hostname could not be determined. Please specify the service principal\n",
                    LogTime(), PROGRAM);
            fprintf(stdout, "BH hostname error\n");
            exit(EXIT_FAILURE);
        }
        service.value = xmalloc(strlen(service_name) + strlen(host_name) + 2);
        snprintf((char *) service.value, strlen(service_name) + strlen(host_name) + 2,
                 "%s@%s", service_name, host_name);
        service.length = strlen((char *) service.value);
        xfree(host_name);
    }

    if (rcache_type) {
        rcache_type_env = (char *) xmalloc(strlen("KRB5RCACHETYPE=")+strlen(rcache_type)+1);
        strcpy(rcache_type_env, "KRB5RCACHETYPE=");
        strcat(rcache_type_env, rcache_type);
        putenv(rcache_type_env);
        debug((char *) "%s| %s: INFO: Setting replay cache type to %s\n",
              LogTime(), PROGRAM, rcache_type);
    }

    if (rcache_dir) {
        rcache_dir_env = (char *) xmalloc(strlen("KRB5RCACHEDIR=")+strlen(rcache_dir)+1);
        strcpy(rcache_dir_env, "KRB5RCACHEDIR=");
        strcat(rcache_dir_env, rcache_dir);
        putenv(rcache_dir_env);
        debug((char *) "%s| %s: INFO: Setting replay cache directory to %s\n",
              LogTime(), PROGRAM, rcache_dir);
    }

    if (keytab_name) {
        keytab_name_env = (char *) xmalloc(strlen("KRB5_KTNAME=")+strlen(keytab_name)+1);
        strcpy(keytab_name_env, "KRB5_KTNAME=");
        strcat(keytab_name_env, keytab_name);
        putenv(keytab_name_env);
    } else {
        keytab_name_env = getenv("KRB5_KTNAME");
        if (!keytab_name_env) {
            ret = krb5_init_context(&context);
            if (!check_k5_err(context, "krb5_init_context", ret)) {
                krb5_kt_default_name(context, default_keytab, MAXPATHLEN);
            }
            keytab_name = xstrdup(default_keytab);
            krb5_free_context(context);
        } else
            keytab_name = xstrdup(keytab_name_env);
    }
    debug((char *) "%s| %s: INFO: Setting keytab to %s\n", LogTime(), PROGRAM, keytab_name);
#if HAVE_KRB5_MEMORY_KEYTAB
    ret = krb5_init_context(&context);
    if (!check_k5_err(context, "krb5_init_context", ret)) {
        memory_keytab_name = (char *)xmalloc(strlen("MEMORY:negotiate_kerberos_auth_")+16);
        snprintf(memory_keytab_name, strlen("MEMORY:negotiate_kerberos_auth_")+16,
                 "MEMORY:negotiate_kerberos_auth_%d", (unsigned int) getpid());
        ret = krb5_read_keytab(context, keytab_name, &ktlist);
        if (check_k5_err(context, "krb5_read_keytab", ret)) {
            debug((char *) "%s| %s: ERROR: Reading keytab %s into list failed\n",
                  LogTime(), PROGRAM, keytab_name);
        } else {
            ret = krb5_write_keytab(context, ktlist, memory_keytab_name);
            if (check_k5_err(context, "krb5_write_keytab", ret)) {
                debug((char *) "%s| %s: ERROR: Writing list into keytab %s\n",
                      LogTime(), PROGRAM, memory_keytab_name);
            } else {
                memory_keytab_name_env = (char *) xmalloc(strlen("KRB5_KTNAME=")+strlen(memory_keytab_name)+1);
                strcpy(memory_keytab_name_env, "KRB5_KTNAME=");
                strcat(memory_keytab_name_env, memory_keytab_name);
                putenv(memory_keytab_name_env);
                xfree(keytab_name);
                keytab_name = xstrdup(memory_keytab_name);
                debug((char *) "%s| %s: INFO: Changed keytab to %s\n",
                      LogTime(), PROGRAM, memory_keytab_name);
            }
        }
        ret = krb5_free_kt_list(context,ktlist);
        if (check_k5_err(context, "krb5_free_kt_list", ret)) {
            debug((char *) "%s| %s: ERROR: Freeing list failed\n",
                  LogTime(), PROGRAM);
        }
    }
    krb5_free_context(context);
#endif
#ifdef HAVE_HEIMDAL_KERBEROS
    gsskrb5_register_acceptor_identity(keytab_name);
#endif
    while (1) {
        if (fgets(buf, sizeof(buf) - 1, stdin) == nullptr) {
            if (ferror(stdin)) {
                debug((char *) "%s| %s: FATAL: fgets() failed! dying..... errno=%d (%s)\n",
                      LogTime(), PROGRAM, ferror(stdin),
                      strerror(ferror(stdin)));

                fprintf(stdout, "BH input error\n");
                exit(EXIT_FAILURE);    /* BIIG buffer */
            }
            fprintf(stdout, "BH input error\n");
            exit(EXIT_SUCCESS);
        }
        c = (char *) memchr(buf, '\n', sizeof(buf) - 1);
        if (c) {
            *c = '\0';
            length = c - buf;
        } else {
            err = 1;
        }
        if (err) {
            debug((char *) "%s| %s: ERROR: Oversized message\n", LogTime(), PROGRAM);
            fprintf(stdout, "BH Oversized message\n");
            err = 0;
            continue;
        }
        debug((char *) "%s| %s: DEBUG: Got '%s' from squid (length: %ld).\n", LogTime(), PROGRAM, buf, length);

        if (buf[0] == '\0') {
            debug((char *) "%s| %s: ERROR: Invalid request\n", LogTime(), PROGRAM);
            fprintf(stdout, "BH Invalid request\n");
            continue;
        }
        if (strlen(buf) < 2) {
            debug((char *) "%s| %s: ERROR: Invalid request [%s]\n", LogTime(), PROGRAM, buf);
            fprintf(stdout, "BH Invalid request\n");
            continue;
        }
        if (!strncmp(buf, "QQ", 2)) {
            gss_release_buffer(&minor_status, &input_token);
            gss_release_buffer(&minor_status, &output_token);
            gss_release_buffer(&minor_status, &service);
            gss_release_cred(&minor_status, &server_creds);
            if (server_name)
                gss_release_name(&minor_status, &server_name);
            if (client_name)
                gss_release_name(&minor_status, &client_name);
            if (gss_context != GSS_C_NO_CONTEXT)
                gss_delete_sec_context(&minor_status, &gss_context, nullptr);
            if (kerberosToken) {
                /* Allocated by parseNegTokenInit, but no matching free function exists.. */
                if (!spnego_flag)
                    xfree(kerberosToken);
            }
            if (spnego_flag) {
                /* Allocated by makeNegTokenTarg, but no matching free function exists.. */
                xfree(spnegoToken);
            }
            xfree(token);
            xfree(rcache_type);
            xfree(rcache_type_env);
            xfree(rcache_dir);
            xfree(rcache_dir_env);
            xfree(keytab_name);
            xfree(keytab_name_env);
#if HAVE_KRB5_MEMORY_KEYTAB
            krb5_kt_close(context, memory_keytab);
            xfree(memory_keytab_name);
            xfree(memory_keytab_name_env);
#endif
            xfree(rfc_user);
            fprintf(stdout, "BH quit command\n");
            exit(EXIT_SUCCESS);
        }
        if (strncmp(buf, "YR", 2) && strncmp(buf, "KK", 2)) {
            debug((char *) "%s| %s: ERROR: Invalid request [%s]\n", LogTime(), PROGRAM, buf);
            fprintf(stdout, "BH Invalid request\n");
            continue;
        }
        if (!strncmp(buf, "YR", 2)) {
            if (gss_context != GSS_C_NO_CONTEXT)
                gss_delete_sec_context(&minor_status, &gss_context, nullptr);
            gss_context = GSS_C_NO_CONTEXT;
        }
        if (strlen(buf) <= 3) {
            debug((char *) "%s| %s: ERROR: Invalid negotiate request [%s]\n", LogTime(), PROGRAM, buf);
            fprintf(stdout, "BH Invalid negotiate request\n");
            continue;
        }
        const char *b64Token = buf+3;
        const size_t srcLen = strlen(buf+3);
        input_token.length = BASE64_DECODE_LENGTH(srcLen);
        debug((char *) "%s| %s: DEBUG: Decode '%s' (decoded length estimate: %d).\n",
              LogTime(), PROGRAM, b64Token, (int) input_token.length);
        input_token.value = xmalloc(input_token.length);

        struct base64_decode_ctx ctx;
        base64_decode_init(&ctx);
        size_t dstLen = 0;
        if (!base64_decode_update(&ctx, &dstLen, static_cast<uint8_t*>(input_token.value), srcLen, b64Token) ||
                !base64_decode_final(&ctx)) {
            debug((char *) "%s| %s: ERROR: Invalid base64 token [%s]\n", LogTime(), PROGRAM, b64Token);
            fprintf(stdout, "BH Invalid negotiate request token\n");
            continue;
        }
        input_token.length = dstLen;

        if ((input_token.length >= sizeof ntlmProtocol + 1) &&
                (!memcmp(input_token.value, ntlmProtocol, sizeof ntlmProtocol))) {
            debug((char *) "%s| %s: WARNING: received type %d NTLM token\n",
                  LogTime(), PROGRAM,
                  (int) *((unsigned char *) input_token.value +
                          sizeof ntlmProtocol));
            fprintf(stdout, "BH received type %d NTLM token\n",
                    (int) *((unsigned char *) input_token.value +
                            sizeof ntlmProtocol));
            goto cleanup;
        }
        if (service_principal) {
            if (strcasecmp(service_principal, "GSS_C_NO_NAME")) {
                major_status = gss_import_name(&minor_status, &service,
                                               (gss_OID) GSS_C_NULL_OID, &server_name);

            } else {
                server_name = GSS_C_NO_NAME;
                major_status = GSS_S_COMPLETE;
                minor_status = 0;
            }
        } else {
            major_status = gss_import_name(&minor_status, &service,
                                           gss_nt_service_name, &server_name);
        }

        if (check_gss_err(major_status, minor_status, "gss_import_name()", log, 1))
            goto cleanup;

        major_status =
            gss_acquire_cred(&minor_status, server_name, GSS_C_INDEFINITE,
                             GSS_C_NO_OID_SET, GSS_C_ACCEPT, &server_creds, nullptr, nullptr);
        if (check_gss_err(major_status, minor_status, "gss_acquire_cred()", log, 1))
            goto cleanup;

        major_status = gss_accept_sec_context(&minor_status,
                                              &gss_context,
                                              server_creds,
                                              &input_token,
                                              GSS_C_NO_CHANNEL_BINDINGS,
                                              &client_name, nullptr, &output_token, &ret_flags, nullptr, nullptr);

        if (output_token.length) {
            spnegoToken = (const unsigned char *) output_token.value;
            spnegoTokenLength = output_token.length;
            token = (char *) xmalloc((size_t)base64_encode_len(spnegoTokenLength));
            if (token == nullptr) {
                debug((char *) "%s| %s: ERROR: Not enough memory\n", LogTime(), PROGRAM);
                fprintf(stdout, "BH Not enough memory\n");
                goto cleanup;
            }
            struct base64_encode_ctx tokCtx;
            base64_encode_init(&tokCtx);
            size_t blen = base64_encode_update(&tokCtx, token, spnegoTokenLength, reinterpret_cast<const uint8_t*>(spnegoToken));
            blen += base64_encode_final(&tokCtx, token+blen);
            token[blen] = '\0';

            if (check_gss_err(major_status, minor_status, "gss_accept_sec_context()", log, 1))
                goto cleanup;
            if (major_status & GSS_S_CONTINUE_NEEDED) {
                debug((char *) "%s| %s: INFO: continuation needed\n", LogTime(), PROGRAM);
                fprintf(stdout, "TT token=%s\n", token);
                goto cleanup;
            }
            gss_release_buffer(&minor_status, &output_token);
            major_status =
                gss_display_name(&minor_status, client_name, &output_token,
                                 nullptr);

            if (check_gss_err(major_status, minor_status, "gss_display_name()", log, 1))
                goto cleanup;
            user = (char *) xmalloc(output_token.length + 1);
            if (user == nullptr) {
                debug((char *) "%s| %s: ERROR: Not enough memory\n", LogTime(), PROGRAM);
                fprintf(stdout, "BH Not enough memory\n");
                goto cleanup;
            }
            memcpy(user, output_token.value, output_token.length);
            user[output_token.length] = '\0';
            if (norealm && (p = strchr(user, '@')) != nullptr) {
                *p = '\0';
            }

#if HAVE_PAC_SUPPORT
            ret = krb5_init_context(&context);
            if (!check_k5_err(context, "krb5_init_context", ret)) {
#if USE_HEIMDAL_KRB5
#define ADWIN2KPAC 128
                major_status = gsskrb5_extract_authz_data_from_sec_context(&minor_status,
                               gss_context, ADWIN2KPAC, &data_set);
                if (!check_gss_err(major_status, minor_status,
                                   "gsskrb5_extract_authz_data_from_sec_context()", log, 0)) {
                    ret = krb5_pac_parse(context, data_set.value, data_set.length, &pac);
                    gss_release_buffer(&minor_status, &data_set);
                    if (!check_k5_err(context, "krb5_pac_parse", ret)) {
                        ag = get_ad_groups((char *)&ad_groups, context, pac);
                        krb5_pac_free(context, pac);
                    }
                    krb5_free_context(context);
                }
#else
                type_id.value = (void *)"mspac";
                type_id.length = strlen((char *)type_id.value);
#define KRB5PACLOGONINFO        1
                major_status = gss_map_name_to_any(&minor_status, client_name, KRB5PACLOGONINFO, &type_id, (gss_any_t *)&pac);
                if (!check_gss_err(major_status, minor_status, "gss_map_name_to_any()", log, 0)) {
                    ag = get_ad_groups((char *)&ad_groups,context, pac);
                }
                (void)gss_release_any_name_mapping(&minor_status, client_name, &type_id, (gss_any_t *)&pac);
                krb5_free_context(context);
#endif
            }
            if (ag) {
                debug((char *) "%s| %s: DEBUG: Groups %s\n", LogTime(), PROGRAM, ag);
            }
#endif
            rfc_user = rfc1738_escape(user);
#if HAVE_PAC_SUPPORT
            fprintf(stdout, "OK token=%s user=%s %s\n", token, rfc_user, ag?ag:"group=");
#else
            fprintf(stdout, "OK token=%s user=%s\n", token, rfc_user);
#endif
            debug((char *) "%s| %s: DEBUG: OK token=%s user=%s\n", LogTime(), PROGRAM, token, rfc_user);
            if (log)
                fprintf(stderr, "%s| %s: INFO: User %s authenticated\n", LogTime(),
                        PROGRAM, rfc_user);
            goto cleanup;
        } else {
            if (check_gss_err(major_status, minor_status, "gss_accept_sec_context()", log, 1))
                goto cleanup;
            if (major_status & GSS_S_CONTINUE_NEEDED) {
                debug((char *) "%s| %s: INFO: continuation needed\n", LogTime(), PROGRAM);
                // XXX: where to get the server token for delivery to client? token is nullptr here.
                fprintf(stdout, "ERR\n");
                goto cleanup;
            }
            gss_release_buffer(&minor_status, &output_token);
            major_status =
                gss_display_name(&minor_status, client_name, &output_token,
                                 nullptr);

            if (check_gss_err(major_status, minor_status, "gss_display_name()", log, 1))
                goto cleanup;
            /*
             *  Return dummy token AA. May need an extra return tag then AF
             */
            user = (char *) xmalloc(output_token.length + 1);
            if (user == nullptr) {
                debug((char *) "%s| %s: ERROR: Not enough memory\n", LogTime(), PROGRAM);
                fprintf(stdout, "BH Not enough memory\n");
                goto cleanup;
            }
            memcpy(user, output_token.value, output_token.length);
            user[output_token.length] = '\0';
            if (norealm && (p = strchr(user, '@')) != nullptr) {
                *p = '\0';
            }
            rfc_user = rfc1738_escape(user);
#if HAVE_PAC_SUPPORT
            fprintf(stdout, "OK token=%s user=%s %s\n", "AA==", rfc_user, ag?ag:"group=");
#else
            fprintf(stdout, "OK token=%s user=%s\n", "AA==", rfc_user);
#endif
            debug((char *) "%s| %s: DEBUG: OK token=%s user=%s\n", LogTime(), PROGRAM, "AA==", rfc_user);
            if (log)
                fprintf(stderr, "%s| %s: INFO: User %s authenticated\n", LogTime(),
                        PROGRAM, rfc_user);
        }
cleanup:
        gss_release_buffer(&minor_status, &input_token);
        gss_release_buffer(&minor_status, &output_token);
        gss_release_cred(&minor_status, &server_creds);
        if (server_name)
            gss_release_name(&minor_status, &server_name);
        if (client_name)
            gss_release_name(&minor_status, &client_name);
        if (kerberosToken) {
            /* Allocated by parseNegTokenInit, but no matching free function exists.. */
            if (!spnego_flag)
                safe_free(kerberosToken);
        }
        if (spnego_flag) {
            /* Allocated by makeNegTokenTarg, but no matching free function exists.. */
            safe_free(spnegoToken);
        }
        safe_free(token);
        safe_free(user);
        continue;
    }
    return EXIT_SUCCESS;
}
#else
#include <cstdlib>
#ifndef MAX_AUTHTOKEN_LEN
#define MAX_AUTHTOKEN_LEN   65535
#endif
int
main(int argc, char *const argv[])
{
    setbuf(stdout, nullptr);
    setbuf(stdin, nullptr);
    char buf[MAX_AUTHTOKEN_LEN];
    while (1) {
        if (fgets(buf, sizeof(buf) - 1, stdin) == NULL) {
            fprintf(stdout, "BH input error\n");
            exit(EXIT_SUCCESS);
        }
        fprintf(stdout, "BH Kerberos authentication not supported\n");
    }
    return EXIT_SUCCESS;
}
#endif /* HAVE_GSSAPI */