/*
 * 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.
 */

/* DEBUG: section 29    Authenticator */

/* The functions in this file handle authentication.
 * They DO NOT perform access control or auditing.
 * See acl.c for access control and client_side.c for auditing */

#include "squid.h"
#include "acl/FilledChecklist.h"
#include "auth/Config.h"
#include "client_side.h"
#include "comm/Connection.h"
#include "fatal.h"
#include "format/Format.h"
#include "helper.h"
#include "helper/Reply.h"
#include "http/Stream.h"
#include "HttpReply.h"
#include "HttpRequest.h"
#include "MemBuf.h"

/* Generic Functions */

char const *
Auth::UserRequest::username() const
{
    if (user() != nullptr)
        return user()->username();
    else
        return nullptr;
}

/**** PUBLIC FUNCTIONS (ALL GENERIC!)  ****/

/* send the initial data to an authenticator module */
void
Auth::UserRequest::start(HttpRequest *request, AccessLogEntry::Pointer &al, AUTHCB * handler, void *data)
{
    assert(handler);
    assert(data);
    debugs(29, 9, this);
    startHelperLookup(request, al, handler, data);
}

bool
Auth::UserRequest::valid() const
{
    debugs(29, 9, "Validating Auth::UserRequest '" << this << "'.");

    if (user() == nullptr) {
        debugs(29, 4, "No associated Auth::User data");
        return false;
    }

    if (user()->auth_type == Auth::AUTH_UNKNOWN) {
        debugs(29, 4, "Auth::User '" << user() << "' uses unknown scheme.");
        return false;
    }

    if (user()->auth_type == Auth::AUTH_BROKEN) {
        debugs(29, 4, "Auth::User '" << user() << "' is broken for it's scheme.");
        return false;
    }

    /* any other sanity checks that we need in the future */

    /* finally return ok */
    debugs(29, 5, "Validated. Auth::UserRequest '" << this << "'.");
    return true;
}

void *
Auth::UserRequest::operator new (size_t)
{
    fatal("Auth::UserRequest not directly allocatable\n");
    return (void *)1;
}

void
Auth::UserRequest::operator delete (void *)
{
    fatal("Auth::UserRequest child failed to override operator delete\n");
}

Auth::UserRequest::UserRequest():
    _auth_user(nullptr),
    message(nullptr),
    lastReply(AUTH_ACL_CANNOT_AUTHENTICATE)
{
    debugs(29, 5, "initialised request " << this);
}

Auth::UserRequest::~UserRequest()
{
    assert(LockCount()==0);
    debugs(29, 5, "freeing request " << this);

    if (user() != nullptr) {
        /* release our references to the user credentials */
        user(nullptr);
    }

    safe_free(message);
}

void
Auth::UserRequest::setDenyMessage(char const *aString)
{
    safe_free(message);
    message = xstrdup(aString);
}

char const *
Auth::UserRequest::getDenyMessage() const
{
    return message;
}

char const *
Auth::UserRequest::denyMessage(char const * const default_message) const
{
    if (getDenyMessage() == nullptr)
        return default_message;

    return getDenyMessage();
}

static void
authenticateAuthUserRequestSetIp(Auth::UserRequest::Pointer auth_user_request, Ip::Address &ipaddr)
{
    Auth::User::Pointer auth_user = auth_user_request->user();

    if (!auth_user)
        return;

    auth_user->addIp(ipaddr);
}

void
authenticateAuthUserRequestRemoveIp(Auth::UserRequest::Pointer auth_user_request, Ip::Address const &ipaddr)
{
    Auth::User::Pointer auth_user = auth_user_request->user();

    if (!auth_user)
        return;

    auth_user->removeIp(ipaddr);
}

void
authenticateAuthUserRequestClearIp(Auth::UserRequest::Pointer auth_user_request)
{
    if (auth_user_request != nullptr)
        auth_user_request->user()->clearIp();
}

int
authenticateAuthUserRequestIPCount(Auth::UserRequest::Pointer auth_user_request)
{
    assert(auth_user_request != nullptr);
    assert(auth_user_request->user() != nullptr);
    return auth_user_request->user()->ipcount;
}

/*
 * authenticateUserAuthenticated: is this auth_user structure logged in ?
 */
int
authenticateUserAuthenticated(Auth::UserRequest::Pointer auth_user_request)
{
    if (auth_user_request == nullptr || !auth_user_request->valid())
        return 0;

    return auth_user_request->authenticated();
}

Auth::Direction
Auth::UserRequest::direction()
{
    if (user() == nullptr)
        return Auth::CRED_ERROR; // No credentials. Should this be a CHALLENGE instead?

    if (authenticateUserAuthenticated(this))
        return Auth::CRED_VALID;

    return module_direction();
}

void
Auth::UserRequest::addAuthenticationInfoHeader(HttpReply *, int)
{}

void
Auth::UserRequest::addAuthenticationInfoTrailer(HttpReply *, int)
{}

void
Auth::UserRequest::releaseAuthServer()
{}

const char *
Auth::UserRequest::connLastHeader()
{
    fatal("Auth::UserRequest::connLastHeader should always be overridden by conn based auth schemes");
    return nullptr;
}

/*
 * authenticateAuthenticateUser: call the module specific code to
 * log this user request in.
 * Cache hits may change the auth_user pointer in the structure if needed.
 * This is basically a handle approach.
 */
static void
authenticateAuthenticateUser(Auth::UserRequest::Pointer auth_user_request, HttpRequest * request, ConnStateData * conn, Http::HdrType type)
{
    assert(auth_user_request.getRaw() != nullptr);

    auth_user_request->authenticate(request, conn, type);
}

static Auth::UserRequest::Pointer
authTryGetUser(Auth::UserRequest::Pointer auth_user_request, ConnStateData * conn, HttpRequest * request)
{
    Auth::UserRequest::Pointer res;

    if (auth_user_request != nullptr)
        res = auth_user_request;
    else if (request != nullptr && request->auth_user_request != nullptr)
        res = request->auth_user_request;
    else if (conn != nullptr)
        res = conn->getAuth();

    // attach the credential notes from helper to the transaction
    if (request != nullptr && res != nullptr && res->user() != nullptr) {
        // XXX: we have no access to the transaction / AccessLogEntry so can't SyncNotes().
        // workaround by using anything already set in HttpRequest
        // OR use new and rely on a later Sync copying these to AccessLogEntry

        UpdateRequestNotes(conn, *request, res->user()->notes);
    }

    return res;
}

/* returns one of
 * AUTH_ACL_CHALLENGE,
 * AUTH_ACL_HELPER,
 * AUTH_ACL_CANNOT_AUTHENTICATE,
 * AUTH_AUTHENTICATED
 *
 * How to use: In your proxy-auth dependent acl code, use the following
 * construct:
 * int rv;
 * if ((rv = AuthenticateAuthenticate()) != AUTH_AUTHENTICATED)
 *   return rv;
 *
 * when this code is reached, the request/connection is authenticated.
 *
 * if you have non-acl code, but want to force authentication, you need a
 * callback mechanism like the acl testing routines that will send a 40[1|7] to
 * the client when rv==AUTH_ACL_CHALLENGE, and will communicate with
 * the authenticateStart routine for rv==AUTH_ACL_HELPER
 *
 * Caller is responsible for locking and unlocking their *auth_user_request!
 */
AuthAclState
Auth::UserRequest::authenticate(Auth::UserRequest::Pointer * auth_user_request, Http::HdrType headertype, HttpRequest * request, ConnStateData * conn, Ip::Address &src_addr, AccessLogEntry::Pointer &al)
{
    const char *proxy_auth;
    assert(headertype != 0);

    proxy_auth = request->header.getStr(headertype);

    /*
     * a note on proxy_auth logix here:
     * proxy_auth==NULL -> unauthenticated request || already
     * authenticated connection so we test for an authenticated
     * connection when we receive no authentication header.
     */

    /* a) can we find other credentials to use? and b) are they logged in already? */
    if (proxy_auth == nullptr && !authenticateUserAuthenticated(authTryGetUser(*auth_user_request,conn,request))) {
        /* no header or authentication failed/got corrupted - restart */
        debugs(29, 4, "No Proxy-Auth header and no working alternative. Requesting auth header.");

        /* something wrong with the AUTH credentials. Force a new attempt */

        /* connection auth we must reset on auth errors */
        if (conn != nullptr) {
            conn->setAuth(nullptr, "HTTP request missing credentials");
        }

        *auth_user_request = nullptr;
        return AUTH_ACL_CHALLENGE;
    }

    /*
     * Is this an already authenticated connection with a new auth header?
     * No check for function required in the if: its compulsory for conn based
     * auth modules
     */
    if (proxy_auth && conn != nullptr && conn->getAuth() != nullptr &&
            authenticateUserAuthenticated(conn->getAuth()) &&
            conn->getAuth()->connLastHeader() != nullptr &&
            strcmp(proxy_auth, conn->getAuth()->connLastHeader())) {
        debugs(29, 2, "WARNING: DUPLICATE AUTH - authentication header on already authenticated connection!. AU " <<
               conn->getAuth() << ", Current user '" <<
               conn->getAuth()->username() << "' proxy_auth " <<
               proxy_auth);

        /* remove this request struct - the link is already authed and it can't be to reauth. */

        /* This should _only_ ever occur on the first pass through
         * authenticateAuthenticate
         */
        assert(*auth_user_request == nullptr);
        conn->setAuth(nullptr, "changed credentials token");
    }

    /* we have a proxy auth header and as far as we know this connection has
     * not had bungled connection oriented authentication happen on it. */
    debugs(29, 9, "header " << (proxy_auth ? proxy_auth : "-") << ".");

    if (*auth_user_request == nullptr) {
        if (conn != nullptr) {
            debugs(29, 9, "This is a new checklist test on:" << conn->clientConnection);
        }

        if (proxy_auth && request->auth_user_request == nullptr && conn != nullptr && conn->getAuth() != nullptr) {
            Auth::SchemeConfig * scheme = Auth::SchemeConfig::Find(proxy_auth);

            if (conn->getAuth()->user() == nullptr || conn->getAuth()->user()->config != scheme) {
                debugs(29, DBG_IMPORTANT, "WARNING: Unexpected change of authentication scheme from '" <<
                       (conn->getAuth()->user()!=nullptr?conn->getAuth()->user()->config->type():"[no user]") <<
                       "' to '" << proxy_auth << "' (client " <<
                       src_addr << ")");

                conn->setAuth(nullptr, "changed auth scheme");
            }
        }

        if (request->auth_user_request == nullptr && (conn == nullptr || conn->getAuth() == nullptr)) {
            /* beginning of a new request check */
            debugs(29, 4, "No connection authentication type");

            *auth_user_request = Auth::SchemeConfig::CreateAuthUser(proxy_auth, al);
            if (*auth_user_request == nullptr)
                return AUTH_ACL_CHALLENGE;
            else if (!(*auth_user_request)->valid()) {
                /* the decode might have left a username for logging, or a message to
                 * the user */

                if ((*auth_user_request)->username()) {
                    request->auth_user_request = *auth_user_request;
                }

                *auth_user_request = nullptr;
                return AUTH_ACL_CHALLENGE;
            }

        } else if (request->auth_user_request != nullptr) {
            *auth_user_request = request->auth_user_request;
        } else {
            assert (conn != nullptr);
            if (conn->getAuth() != nullptr) {
                *auth_user_request = conn->getAuth();
            } else {
                /* failed connection based authentication */
                debugs(29, 4, "Auth user request " << *auth_user_request << " conn-auth missing and failed to authenticate.");
                *auth_user_request = nullptr;
                return AUTH_ACL_CHALLENGE;
            }
        }
    }

    if (!authenticateUserAuthenticated(*auth_user_request)) {
        /* User not logged in. Try to log them in */
        authenticateAuthenticateUser(*auth_user_request, request, conn, headertype);

        switch ((*auth_user_request)->direction()) {

        case Auth::CRED_CHALLENGE:

            if (request->auth_user_request == nullptr) {
                request->auth_user_request = *auth_user_request;
            }
            *auth_user_request = nullptr;
            return AUTH_ACL_CHALLENGE;

        case Auth::CRED_ERROR:
            /* this ACL check is finished. */
            *auth_user_request = nullptr;
            return AUTH_ACL_CHALLENGE;

        case Auth::CRED_LOOKUP:
            /* we are partway through authentication within squid,
             * the *auth_user_request variables stores the auth_user_request
             * for the callback to here - Do not Unlock */
            return AUTH_ACL_HELPER;

        case Auth::CRED_VALID:
            /* authentication is finished */
            /* See if user authentication failed for some reason */
            if (!authenticateUserAuthenticated(*auth_user_request)) {
                if ((*auth_user_request)->username()) {
                    if (!request->auth_user_request) {
                        request->auth_user_request = *auth_user_request;
                    }
                }

                *auth_user_request = nullptr;
                return AUTH_ACL_CHALLENGE;
            }
            // otherwise fallthrough to acceptance.
        }
    }

    /* copy username to request for logging on client-side */
    /* the credentials are correct at this point */
    if (request->auth_user_request == nullptr) {
        request->auth_user_request = *auth_user_request;
        authenticateAuthUserRequestSetIp(*auth_user_request, src_addr);
    }

    return AUTH_AUTHENTICATED;
}

AuthAclState
Auth::UserRequest::tryToAuthenticateAndSetAuthUser(Auth::UserRequest::Pointer * aUR, Http::HdrType headertype, HttpRequest * request, ConnStateData * conn, Ip::Address &src_addr, AccessLogEntry::Pointer &al)
{
    // If we have already been called, return the cached value
    Auth::UserRequest::Pointer t = authTryGetUser(*aUR, conn, request);

    if (t != nullptr && t->lastReply != AUTH_ACL_CANNOT_AUTHENTICATE && t->lastReply != AUTH_ACL_HELPER) {
        if (*aUR == nullptr)
            *aUR = t;

        if (request->auth_user_request == nullptr && t->lastReply == AUTH_AUTHENTICATED) {
            request->auth_user_request = t;
        }
        return t->lastReply;
    }

    // ok, call the actual authenticator routine.
    AuthAclState result = authenticate(aUR, headertype, request, conn, src_addr, al);

    // auth process may have changed the UserRequest we are dealing with
    t = authTryGetUser(*aUR, conn, request);

    if (t != nullptr && result != AUTH_ACL_CANNOT_AUTHENTICATE && result != AUTH_ACL_HELPER)
        t->lastReply = result;

    return result;
}

static Auth::ConfigVector &
schemesConfig(HttpRequest *request, HttpReply *rep)
{
    if (!Auth::TheConfig.schemeLists.empty() && Auth::TheConfig.schemeAccess) {
        ACLFilledChecklist ch(nullptr, request, nullptr);
        ch.reply = rep;
        HTTPMSGLOCK(ch.reply);
        const auto answer = ch.fastCheck(Auth::TheConfig.schemeAccess);
        if (answer.allowed())
            return Auth::TheConfig.schemeLists.at(answer.kind).authConfigs;
    }
    return Auth::TheConfig.schemes;
}

void
Auth::UserRequest::AddReplyAuthHeader(HttpReply * rep, Auth::UserRequest::Pointer auth_user_request, HttpRequest * request, int accelerated, int internal)
/* send the auth types we are configured to support (and have compiled in!) */
{
    Http::HdrType type;

    switch (rep->sline.status()) {

    case Http::scProxyAuthenticationRequired:
        /* Proxy authorisation needed */
        type = Http::HdrType::PROXY_AUTHENTICATE;
        break;

    case Http::scUnauthorized:
        /* WWW Authorisation needed */
        type = Http::HdrType::WWW_AUTHENTICATE;
        break;

    default:
        /* Keep GCC happy */
        /* some other HTTP status */
        type = Http::HdrType::BAD_HDR;
        break;
    }

    debugs(29, 9, "headertype:" << type << " authuser:" << auth_user_request);

    if (((rep->sline.status() == Http::scProxyAuthenticationRequired)
            || (rep->sline.status() == Http::scUnauthorized)) && internal)
        /* this is a authenticate-needed response */
    {

        if (auth_user_request != nullptr && auth_user_request->direction() == Auth::CRED_CHALLENGE)
            /* add the scheme specific challenge header to the response */
            auth_user_request->user()->config->fixHeader(auth_user_request, rep, type, request);
        else {
            /* call each configured & running auth scheme */
            Auth::ConfigVector &configs = schemesConfig(request, rep);
            for (auto *scheme : configs) {
                if (scheme->active()) {
                    if (auth_user_request != nullptr && auth_user_request->scheme()->type() == scheme->type())
                        scheme->fixHeader(auth_user_request, rep, type, request);
                    else
                        scheme->fixHeader(nullptr, rep, type, request);
                } else
                    debugs(29, 4, "Configured scheme " << scheme->type() << " not Active");
            }
        }

    }

    /*
     * allow protocol specific headers to be _added_ to the existing
     * response - currently Digest or Negotiate auth
     */
    if (auth_user_request != nullptr) {
        auth_user_request->addAuthenticationInfoHeader(rep, accelerated);
        if (auth_user_request->lastReply != AUTH_AUTHENTICATED)
            auth_user_request->lastReply = AUTH_ACL_CANNOT_AUTHENTICATE;
    }
}

Auth::Scheme::Pointer
Auth::UserRequest::scheme() const
{
    return Auth::Scheme::Find(user()->config->type());
}

const char *
Auth::UserRequest::helperRequestKeyExtras(HttpRequest *request, AccessLogEntry::Pointer &al)
{
    if (Format::Format *reqFmt = user()->config->keyExtras) {
        static MemBuf mb;
        mb.reset();
        // We should pass AccessLogEntry as second argument ....
        Auth::UserRequest::Pointer oldReq = request->auth_user_request;
        request->auth_user_request = this;
        reqFmt->assemble(mb, al, 0);
        request->auth_user_request = oldReq;
        debugs(29, 5, "Assembled line to send :" << mb.content());
        return mb.content();
    }
    return nullptr;
}

void
Auth::UserRequest::denyMessageFromHelper(const char *proto, const Helper::Reply &reply)
{
    static SBuf messageNote;
    if (!reply.notes.find(messageNote, "message")) {
        messageNote.append(proto);
        messageNote.append(" Authentication denied with no reason given");
    }
    setDenyMessage(messageNote.c_str());
}

