/*
 * Copyright (C) 1996-2023 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.
 */

/*
 * (C) 2000 Francesco Chemolli <kinkie@kame.usr.dsi.unimi.it>
 * Distributed freely under the terms of the GNU General Public License,
 * version 2 or later. See the file COPYING for licensing details
 *
 * 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, USA.
 */

#include "squid.h"
#include "base64.h"
#include "compat/debug.h"
#include "helper/protocol_defines.h"
#include "ntlmauth/ntlmauth.h"
#include "ntlmauth/support_bits.cci"
#include "rfcnb/rfcnb.h"
#include "smblib/smblib.h"

#include <cassert>
#include <cctype>
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <ctime>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#if HAVE_GETOPT_H
#include <getopt.h>
#endif
#if HAVE_UNISTD_H
#include <unistd.h>
#endif

/************* CONFIGURATION ***************/

#define DEAD_DC_RETRY_INTERVAL 30

/************* END CONFIGURATION ***************/

/* A couple of harmless helper macros */
#define SEND(X) debug("sending '%s' to squid\n",X); printf(X "\n");
#ifdef __GNUC__
#define SEND2(X,Y...) debug("sending '" X "' to squid\n",Y); printf(X "\n",Y);
#define SEND3(X,Y...) debug("sending '" X "' to squid\n",Y); printf(X "\n",Y);
#else
/* no gcc, no debugging. varargs macros are a gcc extension */
#define SEND2 printf
#define SEND3 printf
#endif

const char *make_challenge(char *domain, char *controller);
char *ntlm_check_auth(ntlm_authenticate * auth, int auth_length);
void dc_disconnect(void);
int connectedp(void);
int is_dc_ok(char *domain, char *domain_controller);

typedef struct _dc dc;
struct _dc {
    char *domain;
    char *controller;
    time_t dead;        /* 0 if it's alive, otherwise time of death */
    dc *next;
};

/* local functions */
void usage(void);
void process_options(int argc, char *argv[]);
const char * obtain_challenge(void);
void manage_request(void);

#define ENCODED_PASS_LEN 24
#define MAX_USERNAME_LEN 255
#define MAX_DOMAIN_LEN 255
#define MAX_PASSWD_LEN 31

static unsigned char challenge[NTLM_NONCE_LEN];
static unsigned char lmencoded_empty_pass[ENCODED_PASS_LEN],
       ntencoded_empty_pass[ENCODED_PASS_LEN];
SMB_Handle_Type handle = nullptr;
int ntlm_errno;
static char credentials[MAX_USERNAME_LEN+MAX_DOMAIN_LEN+2]; /* we can afford to waste */
static char my_domain[100], my_domain_controller[100];
static char errstr[1001];
#if DEBUG
char error_messages_buffer[NTLM_BLOB_BUFFER_SIZE];
#endif
char load_balance = 0, protocol_pedantic = 0;
dc *controllers = nullptr;
int numcontrollers = 0;
dc *current_dc;
char smb_error_buffer[1000];

/* Disconnects from the DC. A reconnection will be done upon the next request
 */
void
dc_disconnect()
{
    if (handle != NULL)
        SMB_Discon(handle, 0);
    handle = nullptr;
}

int
connectedp()
{
    return (handle != NULL);
}

/* Tries to connect to a DC. Returns 0 on failure, 1 on OK */
int
is_dc_ok(char *domain, char *domain_controller)
{
    SMB_Handle_Type h = SMB_Connect_Server(nullptr, domain_controller, domain);
    if (h == NULL)
        return 0;
    SMB_Discon(h, 0);
    return 1;
}

/* returns 0 on success, > 0 on failure */
static int
init_challenge(char *domain, char *domain_controller)
{
    int smberr;

    if (handle != NULL) {
        return 0;
    }
    debug("Connecting to server %s domain %s\n", domain_controller, domain);
    handle = SMB_Connect_Server(nullptr, domain_controller, domain);
    smberr = SMB_Get_Last_Error();
    SMB_Get_Error_Msg(smberr, errstr, 1000);

    if (handle == NULL) {   /* couldn't connect */
        debug("Couldn't connect to SMB Server. Error:%s\n", errstr);
        return 1;
    }
    if (SMB_Negotiate(handle, SMB_Prots) < 0) {     /* An error */
        debug("Error negotiating protocol with SMB Server\n");
        SMB_Discon(handle, 0);
        handle = nullptr;
        return 2;
    }
    if (handle->Security == 0) {    /* share-level security, unusable */
        debug("SMB Server uses share-level security .. we need user security.\n");
        SMB_Discon(handle, 0);
        handle = nullptr;
        return 3;
    }
    memcpy(challenge, handle->Encrypt_Key, NTLM_NONCE_LEN);
    SMBencrypt((unsigned char *)"",challenge,lmencoded_empty_pass);
    SMBNTencrypt((unsigned char *)"",challenge,ntencoded_empty_pass);
    return 0;
}

const char *
make_challenge(char *domain, char *domain_controller)
{
    /* trying to circumvent some strange problem with pointers in SMBLib */
    /* Ugly as hell, but the lib is going to be dropped... */
    strncpy(my_domain, domain, sizeof(my_domain)-1);
    my_domain[sizeof(my_domain)-1] = '\0';
    strncpy(my_domain_controller, domain_controller, sizeof(my_domain_controller)-1);
    my_domain_controller[sizeof(my_domain_controller)-1] = '\0';

    if (init_challenge(my_domain, my_domain_controller) > 0) {
        return nullptr;
    }

    ntlm_challenge chal;
    uint32_t flags = NTLM_REQUEST_NON_NT_SESSION_KEY |
                     NTLM_CHALLENGE_TARGET_IS_DOMAIN |
                     NTLM_NEGOTIATE_ALWAYS_SIGN |
                     NTLM_NEGOTIATE_USE_NTLM |
                     NTLM_NEGOTIATE_USE_LM |
                     NTLM_NEGOTIATE_ASCII;
    ntlm_make_challenge(&chal, my_domain, my_domain_controller, (char *)challenge, NTLM_NONCE_LEN, flags);

    size_t len = sizeof(chal) - sizeof(chal.payload) + le16toh(chal.target.maxlen);
    // for lack of a good NTLM token size limit, allow up to what the helper input can be
    // validations later will expect to be limited to that size.
    static char b64buf[HELPER_INPUT_BUFFER-10]; /* 10 for other line fields, delimiters and terminator */
    if (base64_encode_len(len) < sizeof(b64buf)-1) {
        debug("base64 encoding of the token challenge will exceed %" PRIuSIZE " bytes", sizeof(b64buf));
        return nullptr;
    }

    struct base64_encode_ctx ctx;
    base64_encode_init(&ctx);
    size_t blen = base64_encode_update(&ctx, b64buf, len, reinterpret_cast<const uint8_t *>(&chal));
    blen += base64_encode_final(&ctx, b64buf+blen);
    b64buf[blen] = '\0';
    return b64buf;
}

/* returns NULL on failure, or a pointer to
 * the user's credentials (domain\\username)
 * upon success. WARNING. It's pointing to static storage.
 * In case of problem sets as side-effect ntlm_errno to one of the
 * codes defined in ntlm.h
 */
char *
ntlm_check_auth(ntlm_authenticate * auth, int auth_length)
{
    int rv;
    char pass[MAX_PASSWD_LEN+1];
    char *domain = credentials;
    char *user;
    lstring tmp;

    if (handle == NULL) {   /*if null we aren't connected, but it shouldn't happen */
        debug("Weird, we've been disconnected\n");
        ntlm_errno = NTLM_ERR_NOT_CONNECTED;
        return nullptr;
    }

    /*      debug("fetching domain\n"); */
    tmp = ntlm_fetch_string(&(auth->hdr), auth_length, &auth->domain, auth->flags);
    if (tmp.str == NULL || tmp.l == 0) {
        debug("No domain supplied. Returning no-auth\n");
        ntlm_errno = NTLM_ERR_LOGON;
        return nullptr;
    }
    if (tmp.l > MAX_DOMAIN_LEN) {
        debug("Domain string exceeds %d bytes, rejecting\n", MAX_DOMAIN_LEN);
        ntlm_errno = NTLM_ERR_LOGON;
        return nullptr;
    }
    memcpy(domain, tmp.str, tmp.l);
    user = domain + tmp.l;
    *user = '\0';
    ++user;

    /*      debug("fetching user name\n"); */
    tmp = ntlm_fetch_string(&(auth->hdr), auth_length, &auth->user, auth->flags);
    if (tmp.str == NULL || tmp.l == 0) {
        debug("No username supplied. Returning no-auth\n");
        ntlm_errno = NTLM_ERR_LOGON;
        return nullptr;
    }
    if (tmp.l > MAX_USERNAME_LEN) {
        debug("Username string exceeds %d bytes, rejecting\n", MAX_USERNAME_LEN);
        ntlm_errno = NTLM_ERR_LOGON;
        return nullptr;
    }
    memcpy(user, tmp.str, tmp.l);
    *(user + tmp.l) = '\0';

    // grab the *response blobs. these are fixed length 24 bytes of binary
    const ntlmhdr *packet = &(auth->hdr);
    {
        const strhdr * str = &auth->lmresponse;

        int16_t len = le16toh(str->len);
        int32_t offset = le32toh(str->offset);

        if (len != ENCODED_PASS_LEN || offset + len > auth_length || offset == 0) {
            debug("LM response: insane data (pkt-sz: %d, fetch len: %d, offset: %d)\n", auth_length, len, offset);
            ntlm_errno = NTLM_ERR_LOGON;
            return nullptr;
        }
        tmp.str = (char *)packet + offset;
        tmp.l = len;
    }
    if (tmp.l > MAX_PASSWD_LEN) {
        debug("Password string exceeds %d bytes, rejecting\n", MAX_PASSWD_LEN);
        ntlm_errno = NTLM_ERR_LOGON;
        return nullptr;
    }

    /* Authenticating against the NT response doesn't seem to work... in SMB LM helper. */
    memcpy(pass, tmp.str, tmp.l);
    pass[min(MAX_PASSWD_LEN,tmp.l)] = '\0';

    debug("Empty LM pass detection: user: '%s', ours:'%s', his: '%s' (length: %d)\n",
          user,lmencoded_empty_pass,tmp.str,tmp.l);
    if (memcmp(tmp.str,lmencoded_empty_pass,ENCODED_PASS_LEN)==0) {
        fprintf(stderr,"Empty LM password supplied for user %s\\%s. "
                "No-auth\n",domain,user);
        ntlm_errno=NTLM_ERR_LOGON;
        return nullptr;
    }

    /* still fetch the NT response and check validity against empty password */
    {
        const strhdr * str = &auth->ntresponse;
        int16_t len = le16toh(str->len);
        // NT response field may be absent. that is okay.
        if (len != 0) {
            int32_t offset = le32toh(str->offset);

            if (len != ENCODED_PASS_LEN || offset + len > auth_length || offset == 0) {
                debug("NT response: insane data (pkt-sz: %d, fetch len: %d, offset: %d)\n", auth_length, len, offset);
                ntlm_errno = NTLM_ERR_LOGON;
                return nullptr;
            }
            tmp.str = (char *)packet + offset;
            tmp.l = len;

            debug("Empty NT pass detection: user: '%s', ours:'%s', his: '%s' (length: %d)\n",
                  user,ntencoded_empty_pass,tmp.str,tmp.l);
            if (memcmp(tmp.str,lmencoded_empty_pass,ENCODED_PASS_LEN)==0) {
                fprintf(stderr,"ERROR: Empty NT password supplied for user %s\\%s. No-auth\n", domain, user);
                ntlm_errno = NTLM_ERR_LOGON;
                return nullptr;
            }
        }
    }

    debug("checking domain: '%s', user: '%s', pass='%s'\n", domain, user, pass);

    rv = SMB_Logon_Server(handle, user, pass, domain, 1);
    debug("Login attempt had result %d\n", rv);

    if (rv != NTLM_ERR_NONE) {  /* failed */
        ntlm_errno = rv;
        return nullptr;
    }
    *(user - 1) = '\\';     /* hack. Performing, but ugly. */

    debug("credentials: %s\n", credentials);
    return credentials;
}

extern "C" void timeout_during_auth(int signum);

static char got_timeout = 0;
/** signal handler to be invoked when the authentication operation
 * times out */
void
timeout_during_auth(int)
{
    dc_disconnect();
}

/*
 * options:
 * -b try load-balancing the domain-controllers
 * -f fail-over to another DC if DC connection fails.
 *    DEPRECATED and VERBOSELY IGNORED. This is on by default now.
 * -l last-ditch-mode
 * domain\controller ...
 */
char *my_program_name = nullptr;

void
usage()
{
    fprintf(stderr,
            "%s usage:\n%s [-b] [-f] [-d] [-l] domain\\controller [domain\\controller ...]\n"
            "-b enables load-balancing among controllers\n"
            "-f enables failover among controllers (DEPRECATED and always active)\n"
            "-d enables debugging statements if DEBUG was defined at build-time.\n\n"
            "You MUST specify at least one Domain Controller.\n"
            "You can use either \\ or / as separator between the domain name \n"
            "and the controller name\n",
            my_program_name, my_program_name);
}

/* int debug_enabled=0; defined in libcompat */

void
process_options(int argc, char *argv[])
{
    int opt, j, had_error = 0;
    dc *new_dc = nullptr, *last_dc = nullptr;
    while (-1 != (opt = getopt(argc, argv, "bfld"))) {
        switch (opt) {
        case 'b':
            load_balance = 1;
            break;
        case 'f':
            fprintf(stderr,
                    "WARNING. The -f flag is DEPRECATED and always active.\n");
            break;
        case 'd':
            debug_enabled=1;
            break;
        default:
            fprintf(stderr, "unknown option: -%c. Exiting\n", opt);
            usage();
            had_error = 1;
        }
    }
    if (had_error)
        exit(EXIT_FAILURE);
    /* Okay, now begin filling controllers up */
    /* we can avoid memcpy-ing, and just reuse argv[] */
    for (j = optind; j < argc; ++j) {
        char *d, *c;
        /* d will not be freed in case of non-error. Since we don't reconfigure,
         * it's going to live as long as the process anyways */
        d = static_cast<char*>(xmalloc(strlen(argv[j]) + 1));
        strcpy(d, argv[j]);
        debug("Adding domain-controller %s\n", d);
        if (NULL == (c = strchr(d, '\\')) && NULL == (c = strchr(d, '/'))) {
            fprintf(stderr, "Couldn't grok domain-controller %s\n", d);
            free(d);
            continue;
        }
        /* more than one delimiter is not allowed */
        if (NULL != strchr(c + 1, '\\') || NULL != strchr(c + 1, '/')) {
            fprintf(stderr, "Broken domain-controller %s\n", d);
            free(d);
            continue;
        }
        *c= '\0';
        ++c;
        new_dc = static_cast<dc *>(xmalloc(sizeof(dc)));
        if (!new_dc) {
            fprintf(stderr, "Malloc error while parsing DC options\n");
            free(d);
            continue;
        }
        /* capitalize */
        uc(c);
        uc(d);
        ++numcontrollers;
        new_dc->domain = d;
        new_dc->controller = c;
        new_dc->dead = 0;
        if (controllers == NULL) {  /* first controller */
            controllers = new_dc;
            last_dc = new_dc;
        } else {
            last_dc->next = new_dc; /* can't be null */
            last_dc = new_dc;
        }
    }
    if (numcontrollers == 0) {
        fprintf(stderr, "You must specify at least one domain-controller!\n");
        usage();
        exit(EXIT_FAILURE);
    }
    last_dc->next = controllers;    /* close the queue, now it's circular */
}

/**
 * tries connecting to the domain controllers in the "controllers" ring,
 * with failover if the adequate option is specified.
 */
const char *
obtain_challenge()
{
    int j = 0;
    const char *ch = nullptr;
    for (j = 0; j < numcontrollers; ++j) {
        debug("obtain_challenge: selecting %s\\%s (attempt #%d)\n",
              current_dc->domain, current_dc->controller, j + 1);
        if (current_dc->dead != 0) {
            if (time(NULL) - current_dc->dead >= DEAD_DC_RETRY_INTERVAL) {
                /* mark helper as retry-worthy if it's so. */
                debug("Reviving DC\n");
                current_dc->dead = 0;
            } else {        /* skip it */
                debug("Skipping it\n");
                continue;
            }
        }
        /* else branch. Here we KNOW that the DC is fine */
        debug("attempting challenge retrieval\n");
        ch = make_challenge(current_dc->domain, current_dc->controller);
        debug("make_challenge retuned %p\n", ch);
        if (ch) {
            debug("Got it\n");
            return ch;      /* All went OK, returning */
        }
        /* Huston, we've got a problem. Take this DC out of the loop */
        debug("Marking DC as DEAD\n");
        current_dc->dead = time(NULL);
        /* Try with the next */
        debug("moving on to next controller\n");
        current_dc = current_dc->next;
    }
    /* all DCs failed. */
    return nullptr;
}

void
manage_request()
{
    ntlmhdr *fast_header;
    char buf[NTLM_BLOB_BUFFER_SIZE];
    char decoded[NTLM_BLOB_BUFFER_SIZE];
    char *ch2, *cred = nullptr;

    if (fgets(buf, NTLM_BLOB_BUFFER_SIZE, stdin) == NULL) {
        fprintf(stderr, "fgets() failed! dying..... errno=%d (%s)\n", errno,
                strerror(errno));
        exit(EXIT_FAILURE);        /* BIIG buffer */
    }
    debug("managing request\n");
    ch2 = (char*)memchr(buf, '\n', NTLM_BLOB_BUFFER_SIZE);  /* safer against overrun than strchr */
    if (ch2) {
        *ch2 = '\0';        /* terminate the string at newline. */
    }
    debug("ntlm authenticator. Got '%s' from Squid\n", buf);

    if (memcmp(buf, "KK ", 3) == 0) {   /* authenticate-request */
        /* figure out what we got */
        struct base64_decode_ctx ctx;
        base64_decode_init(&ctx);
        size_t dstLen = 0;
        int decodedLen = 0;
        if (!base64_decode_update(&ctx, &dstLen, reinterpret_cast<uint8_t*>(decoded), strlen(buf)-3, buf+3) ||
                !base64_decode_final(&ctx)) {
            SEND("NA Packet format error, couldn't base64-decode");
            return;
        }
        decodedLen = dstLen;

        if ((size_t)decodedLen < sizeof(ntlmhdr)) { /* decoding failure, return error */
            SEND("NA Packet format error, truncated packet header.");
            return;
        }
        /* fast-track-decode request type. */
        fast_header = (ntlmhdr *) decoded;

        /* sanity-check: it IS a NTLMSSP packet, isn't it? */
        if (ntlm_validate_packet(fast_header, NTLM_ANY) < 0) {
            SEND("NA Broken authentication packet");
            return;
        }
        switch (le32toh(fast_header->type)) {
        case NTLM_NEGOTIATE:
            SEND("NA Invalid negotiation request received");
            return;
        /* notreached */
        case NTLM_CHALLENGE:
            SEND("NA Got a challenge. We refuse to have our authority disputed");
            return;
        /* notreached */
        case NTLM_AUTHENTICATE:
            /* check against the DC */
            signal(SIGALRM, timeout_during_auth);
            alarm(30);
            cred = ntlm_check_auth((ntlm_authenticate *) decoded, decodedLen);
            alarm(0);
            signal(SIGALRM, SIG_DFL);
            if (got_timeout != 0) {
                fprintf(stderr, "ntlm-auth[%ld]: Timeout during authentication.\n", (long)getpid());
                SEND("BH Timeout during authentication");
                got_timeout = 0;
                return;
            }
            if (cred == NULL) {
                int smblib_err, smb_errorclass, smb_errorcode, nb_error;
                if (ntlm_errno == NTLM_ERR_LOGON) { /* hackish */
                    SEND("NA Logon Failure");
                    return;
                }
                /* there was an error. We have two errno's to look at.
                 * libntlmssp's erno is insufficient, we'll have to look at
                 * the actual SMB library error codes, to actually figure
                 * out what's happening. The thing has braindamaged interfacess..*/
                smblib_err = SMB_Get_Last_Error();
                smb_errorclass = SMBlib_Error_Class(SMB_Get_Last_SMB_Err());
                smb_errorcode = SMBlib_Error_Code(SMB_Get_Last_SMB_Err());
                nb_error = RFCNB_Get_Last_Error();
                debug("No creds. SMBlib error %d, SMB error class %d, SMB error code %d, NB error %d\n",
                      smblib_err, smb_errorclass, smb_errorcode, nb_error);
                /* Should I use smblib_err? Actually it seems I can do as well
                 * without it.. */
                if (nb_error != 0) {    /* netbios-level error */
                    SEND("BH NetBios error!");
                    fprintf(stderr, "NetBios error code %d (%s)\n", nb_error,
                            RFCNB_Error_Strings[abs(nb_error)]);
                    return;
                }
                switch (smb_errorclass) {
                case SMBC_SUCCESS:
                    debug("Huh? Got a SMB success code but could check auth..");
                    SEND("NA Authentication failed");
                    return;
                case SMBC_ERRDOS:
                    /*this is the most important one for errors */
                    debug("DOS error\n");
                    switch (smb_errorcode) {
                    /* two categories matter to us: those which could be
                     * server errors, and those which are auth errors */
                    case SMBD_noaccess: /* 5 */
                        SEND("NA Access denied");
                        return;
                    case SMBD_badformat:
                        SEND("NA bad format in authentication packet");
                        return;
                    case SMBD_badaccess:
                        SEND("NA Bad access request");
                        return;
                    case SMBD_baddata:
                        SEND("NA Bad Data");
                        return;
                    default:
                        SEND("BH DOS Error");
                        return;
                    }
                case SMBC_ERRSRV:   /* server errors */
                    debug("Server error");
                    switch (smb_errorcode) {
                    /* mostly same as above */
                    case SMBV_badpw:
                        SEND("NA Bad password");
                        return;
                    case SMBV_access:
                        SEND("NA Server access error");
                        return;
                    default:
                        SEND("BH Server Error");
                        return;
                    }
                case SMBC_ERRHRD:   /* hardware errors don't really matter */
                    SEND("BH Domain Controller Hardware error");
                    return;
                case SMBC_ERRCMD:
                    SEND("BH Domain Controller Command Error");
                    return;
                }
                SEND("BH unknown internal error.");
                return;
            }

            lc(cred);       /* let's lowercase them for our convenience */
            SEND2("AF %s", cred);
            return;
        default:
            SEND("BH unknown authentication packet type");
            return;
        }
        /* notreached */
        return;
    }
    if (memcmp(buf, "YR", 2) == 0) {    /* refresh-request */
        dc_disconnect();
        const char *ch = obtain_challenge();
        /* Robert says we can afford to wait forever. I'll trust him on this
         * one */
        while (ch == NULL) {
            sleep(30);
            ch = obtain_challenge();
        }
        SEND2("TT %s", ch);
        return;
    }
    SEND("BH Helper detected protocol error");
    return;
    /********* END ********/

}

int
main(int argc, char *argv[])
{
    debug("%s " VERSION " " SQUID_BUILD_INFO " starting up...\n", argv[0]);

    my_program_name = argv[0];
    process_options(argc, argv);

    debug("options processed OK\n");

    /* initialize FDescs */
    setbuf(stdout, nullptr);
    setbuf(stderr, nullptr);

    /* select the first domain controller we're going to use */
    current_dc = controllers;
    if (load_balance != 0 && numcontrollers > 1) {
        int n;
        pid_t pid = getpid();
        n = pid % numcontrollers;
        debug("load balancing. Selected controller #%d\n", n);
        while (n > 0) {
            current_dc = current_dc->next;
            --n;
        }
    }
    while (1) {
        manage_request();
    }
    /* notreached */
    return EXIT_SUCCESS;
}

