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

#include "squid.h"
#include "AccessLogEntry.h"
#include "auth/digest/Config.h"
#include "auth/digest/User.h"
#include "auth/digest/UserRequest.h"
#include "auth/State.h"
#include "format/Format.h"
#include "helper.h"
#include "helper/Reply.h"
#include "HttpHeaderTools.h"
#include "HttpReply.h"
#include "HttpRequest.h"
#include "MemBuf.h"

Auth::Digest::UserRequest::UserRequest() :
    noncehex(nullptr),
    cnonce(nullptr),
    realm(nullptr),
    pszPass(nullptr),
    algorithm(nullptr),
    pszMethod(nullptr),
    qop(nullptr),
    uri(nullptr),
    response(nullptr),
    nonce(nullptr)
{
    memset(nc, 0, sizeof(nc));
    memset(&flags, 0, sizeof(flags));
}

/**
 * Delete the digest request structure.
 * Does NOT delete related AuthUser structures
 */
Auth::Digest::UserRequest::~UserRequest()
{
    assert(LockCount()==0);

    safe_free(noncehex);
    safe_free(cnonce);
    safe_free(realm);
    safe_free(pszPass);
    safe_free(algorithm);
    safe_free(pszMethod);
    safe_free(qop);
    safe_free(uri);
    safe_free(response);

    if (nonce)
        authDigestNonceUnlink(nonce);
}

const char *
Auth::Digest::UserRequest::credentialsStr()
{
    return realm;
}

/** log a digest user in
 */
void
Auth::Digest::UserRequest::authenticate(HttpRequest * request, ConnStateData *, Http::HdrType)
{
    HASHHEX SESSIONKEY;
    HASHHEX HA2 = "";
    HASHHEX Response;

    /* if the check has corrupted the user, just return */
    if (user() == nullptr || user()->credentials() == Auth::Failed) {
        return;
    }

    Auth::User::Pointer auth_user = user();

    Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User*>(auth_user.getRaw());
    assert(digest_user != nullptr);

    Auth::Digest::UserRequest *digest_request = this;

    /* do we have the HA1 */
    if (!digest_user->HA1created) {
        auth_user->credentials(Auth::Pending);
        return;
    }

    if (digest_request->nonce == nullptr) {
        /* this isn't a nonce we issued */
        auth_user->credentials(Auth::Failed);
        return;
    }

    DigestCalcHA1(digest_request->algorithm, nullptr, nullptr, nullptr,
                  authenticateDigestNonceNonceHex(digest_request->nonce),
                  digest_request->cnonce,
                  digest_user->HA1, SESSIONKEY);
    SBuf sTmp = request->method.image();
    DigestCalcResponse(SESSIONKEY, authenticateDigestNonceNonceHex(digest_request->nonce),
                       digest_request->nc, digest_request->cnonce, digest_request->qop,
                       sTmp.c_str(), digest_request->uri, HA2, Response);

    debugs(29, 9, "\nResponse = '" << digest_request->response << "'\nsquid is = '" << Response << "'");

    if (strcasecmp(digest_request->response, Response) != 0) {
        if (!digest_request->flags.helper_queried) {
            /* Query the helper in case the password has changed */
            digest_request->flags.helper_queried = true;
            auth_user->credentials(Auth::Pending);
            return;
        }

        if (static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->PostWorkaround && request->method != Http::METHOD_GET) {
            /* Ugly workaround for certain very broken browsers using the
             * wrong method to calculate the request-digest on POST request.
             * This should be deleted once Digest authentication becomes more
             * widespread and such broken browsers no longer are commonly
             * used.
             */
            sTmp = HttpRequestMethod(Http::METHOD_GET).image();
            DigestCalcResponse(SESSIONKEY, authenticateDigestNonceNonceHex(digest_request->nonce),
                               digest_request->nc, digest_request->cnonce, digest_request->qop,
                               sTmp.c_str(), digest_request->uri, HA2, Response);

            if (strcasecmp(digest_request->response, Response)) {
                auth_user->credentials(Auth::Failed);
                digest_request->flags.invalid_password = true;
                digest_request->setDenyMessage("Incorrect password");
                return;
            } else {
                const char *useragent = request->header.getStr(Http::HdrType::USER_AGENT);

                static Ip::Address last_broken_addr;
                static int seen_broken_client = 0;

                if (!seen_broken_client) {
                    last_broken_addr.setNoAddr();
                    seen_broken_client = 1;
                }

                if (last_broken_addr != request->client_addr) {
                    debugs(29, DBG_IMPORTANT, "ERROR: User agent Digest Authentication POST bug detected from " <<
                           request->client_addr << " using '" <<
                           (useragent ? useragent : "-") <<
                           "'. Please upgrade browser. See Bug #630 for details.");

                    last_broken_addr = request->client_addr;
                }
            }
        } else {
            auth_user->credentials(Auth::Failed);
            digest_request->flags.invalid_password = true;
            digest_request->setDenyMessage("Incorrect password");
            return;
        }
    }

    /* check for stale nonce */
    /* check Auth::Pending to avoid loop */

    if (!authDigestNonceIsValid(digest_request->nonce, digest_request->nc) && user()->credentials() != Auth::Pending) {
        debugs(29, 3, auth_user->username() << "' validated OK but nonce stale: " << digest_request->noncehex);
        /* Pending prevent banner and makes a ldap control */
        auth_user->credentials(Auth::Pending);
        nonce->flags.valid = false;
        authDigestNoncePurge(nonce);
        return;
    }

    auth_user->credentials(Auth::Ok);

    /* password was checked and did match */
    debugs(29, 4, "user '" << auth_user->username() << "' validated OK");
}

Auth::Direction
Auth::Digest::UserRequest::module_direction()
{
    if (user()->auth_type != Auth::AUTH_DIGEST)
        return Auth::CRED_ERROR;

    switch (user()->credentials()) {

    case Auth::Ok:
        return Auth::CRED_VALID;

    case Auth::Handshake:
    case Auth::Failed:
        /* send new challenge */
        return Auth::CRED_CHALLENGE;

    case Auth::Unchecked:
    case Auth::Pending:
        return Auth::CRED_LOOKUP;

    default:
        return Auth::CRED_ERROR;
    }
}

void
Auth::Digest::UserRequest::addAuthenticationInfoHeader(HttpReply * rep, int accel)
{
    Http::HdrType type;

    /* don't add to authentication error pages */
    if ((!accel && rep->sline.status() == Http::scProxyAuthenticationRequired)
            || (accel && rep->sline.status() == Http::scUnauthorized))
        return;

    type = accel ? Http::HdrType::AUTHENTICATION_INFO : Http::HdrType::PROXY_AUTHENTICATION_INFO;

#if WAITING_FOR_TE
    /* test for http/1.1 transfer chunked encoding */
    if (chunkedtest)
        return;
#endif

    if ((static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->authenticateProgram) && authDigestNonceLastRequest(nonce)) {
        flags.authinfo_sent = true;
        Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User *>(user().getRaw());
        if (!digest_user)
            return;

        digest_nonce_h *nextnonce = digest_user->currentNonce();
        if (!nextnonce || authDigestNonceLastRequest(nonce)) {
            nextnonce = authenticateDigestNonceNew();
            authDigestUserLinkNonce(digest_user, nextnonce);
        }
        debugs(29, 9, "Sending type:" << type << " header: 'nextnonce=\"" << authenticateDigestNonceNonceHex(nextnonce) << "\"");
        httpHeaderPutStrf(&rep->header, type, "nextnonce=\"%s\"", authenticateDigestNonceNonceHex(nextnonce));
    }
}

#if WAITING_FOR_TE
void
Auth::Digest::UserRequest::addAuthenticationInfoTrailer(HttpReply * rep, int accel)
{
    int type;

    if (!auth_user_request)
        return;

    /* has the header already been send? */
    if (flags.authinfo_sent)
        return;

    /* don't add to authentication error pages */
    if ((!accel && rep->sline.status() == Http::scProxyAuthenticationRequired)
            || (accel && rep->sline.status() == Http::scUnauthorized))
        return;

    type = accel ? Http::HdrType::AUTHENTICATION_INFO : Http::HdrType::PROXY_AUTHENTICATION_INFO;

    if ((static_cast<Auth::Digest::Config*>(digestScheme::GetInstance()->getConfig())->authenticate) && authDigestNonceLastRequest(nonce)) {
        Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User *>(auth_user_request->user().getRaw());
        nonce = digest_user->currentNonce();
        if (!nonce) {
            nonce = authenticateDigestNonceNew();
            authDigestUserLinkNonce(digest_user, nonce);
        }
        debugs(29, 9, "Sending type:" << type << " header: 'nextnonce=\"" << authenticateDigestNonceNonceHex(nonce) << "\"");
        httpTrailerPutStrf(&rep->header, type, "nextnonce=\"%s\"", authenticateDigestNonceNonceHex(nonce));
    }
}
#endif

/* send the initial data to a digest authenticator module */
void
Auth::Digest::UserRequest::startHelperLookup(HttpRequest *request, AccessLogEntry::Pointer &al, AUTHCB * handler, void *data)
{
    char buf[8192];

    assert(user() != nullptr && user()->auth_type == Auth::AUTH_DIGEST);
    debugs(29, 9, "'\"" << user()->username() << "\":\"" << realm << "\"'");

    if (static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->authenticateProgram == nullptr) {
        debugs(29, DBG_CRITICAL, "ERROR: No Digest authentication program configured.");
        handler(data);
        return;
    }

    const char *keyExtras = helperRequestKeyExtras(request, al);
    if (keyExtras)
        snprintf(buf, 8192, "\"%s\":\"%s\" %s\n", user()->username(), realm, keyExtras);
    else
        snprintf(buf, 8192, "\"%s\":\"%s\"\n", user()->username(), realm);

    helperSubmit(digestauthenticators, buf, Auth::Digest::UserRequest::HandleReply,
                 new Auth::StateData(this, handler, data));
}

void
Auth::Digest::UserRequest::HandleReply(void *data, const Helper::Reply &reply)
{
    Auth::StateData *replyData = static_cast<Auth::StateData *>(data);
    debugs(29, 9, "reply=" << reply);

    assert(replyData->auth_user_request != nullptr);
    Auth::UserRequest::Pointer auth_user_request = replyData->auth_user_request;

    // add new helper kv-pair notes to the credentials object
    // so that any transaction using those credentials can access them
    static const NotePairs::Names appendables = { SBuf("group"), SBuf("nonce"), SBuf("tag") };
    auth_user_request->user()->notes.replaceOrAddOrAppend(&reply.notes, appendables);
    // remove any private credentials detail which got added.
    auth_user_request->user()->notes.remove("ha1");

    static bool oldHelperWarningDone = false;
    switch (reply.result) {
    case Helper::Unknown: {
        // Squid 3.3 and older the digest helper only returns a HA1 hash (no "OK")
        // the HA1 will be found in content() for these responses.
        if (!oldHelperWarningDone) {
            debugs(29, DBG_IMPORTANT, "WARNING: Digest auth helper returned old format HA1 response. It needs to be upgraded.");
            oldHelperWarningDone=true;
        }

        /* allow this because the digest_request pointer is purely local */
        Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User *>(auth_user_request->user().getRaw());
        assert(digest_user != nullptr);

        CvtBin(reply.other().content(), digest_user->HA1);
        digest_user->HA1created = 1;
    }
    break;

    case Helper::Okay: {
        /* allow this because the digest_request pointer is purely local */
        Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User *>(auth_user_request->user().getRaw());
        assert(digest_user != nullptr);

        if (const char *ha1Note = reply.notes.findFirst("ha1")) {
            CvtBin(ha1Note, digest_user->HA1);
            digest_user->HA1created = 1;
        } else {
            debugs(29, DBG_IMPORTANT, "ERROR: Digest auth helper did not produce a HA1. Using the wrong helper program? received: " << reply);
        }
    }
    break;

    case Helper::TT:
        debugs(29, DBG_IMPORTANT, "ERROR: Digest auth does not support the result code received. Using the wrong helper program? received: " << reply);
        [[fallthrough]]; // to handle this as an ERR response

    case Helper::TimedOut:
    case Helper::BrokenHelper:
        [[fallthrough]]; // to (silently) handle this as an ERR response

    // TODO retry the broken lookup on another helper?
    case Helper::Error: {
        /* allow this because the digest_request pointer is purely local */
        Auth::Digest::UserRequest *digest_request = dynamic_cast<Auth::Digest::UserRequest *>(auth_user_request.getRaw());
        assert(digest_request);

        digest_request->user()->credentials(Auth::Failed);
        digest_request->flags.invalid_password = true;

        SBuf msgNote;
        if (reply.notes.find(msgNote, "message")) {
            digest_request->setDenyMessage(msgNote.c_str());
        } else if (reply.other().hasContent()) {
            // old helpers did send ERR result but a bare message string instead of message= key name.
            // TODO deprecate and remove old auth digest helper protocol
            digest_request->setDenyMessage(reply.other().content());
            if (!oldHelperWarningDone) {
                debugs(29, DBG_IMPORTANT, "WARNING: Digest auth helper returned old format ERR response. It needs to be upgraded.");
                oldHelperWarningDone=true;
            }
        }
    }
    break;
    }

    void *cbdata = nullptr;
    if (cbdataReferenceValidDone(replyData->data, &cbdata))
        replyData->handler(cbdata);

    delete replyData;
}

