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

/* DEBUG: section 35    FQDN Cache */

#include "squid.h"
#include "cbdata.h"
#include "dns/forward.h"
#include "dns/LookupDetails.h"
#include "dns/rfc1035.h"
#include "event.h"
#include "fqdncache.h"
#include "helper.h"
#include "mgr/Registration.h"
#include "snmp_agent.h"
#include "SquidConfig.h"
#include "StatCounters.h"
#include "Store.h"
#include "util.h"

#if SQUID_SNMP
#include "snmp_core.h"
#endif

bool Dns::ResolveClientAddressesAsap = false;

/**
 \defgroup FQDNCacheAPI FQDN Cache API
 \ingroup Components
 \section FQDNCacheIntroduction Introduction
 \par
 *  The FQDN cache is a built-in component of squid providing
 *  Hostname to IP-Number translation functionality and managing
 *  the involved data-structures. Efficiency concerns require
 *  mechanisms that allow non-blocking access to these mappings.
 *  The FQDN cache usually doesn't block on a request except for
 *  special cases where this is desired (see below).
 */

/**
 \defgroup FQDNCacheInternal FQDN Cache Internals
 \ingroup FQDNCacheAPI
 \par
 *  Internally, the execution flow is as follows:
 *  On a miss, fqdncache_nbgethostbyaddr() checks whether a request
 *  for this name is already pending, and if positive, it creates a
 *  new entry using fqdncacheAddEntry(). Then it calls
 *  fqdncacheAddPending() to add a request to the queue together
 *  with data and handler.  Else, ifqdncache_dnsDispatch() is called
 *  to directly create a DNS query or to fqdncacheEnqueue() if all
 *  no DNS port is free.
 *
 \par
 *  fqdncacheCallback() is called regularly to walk down the pending
 *  list and call handlers.
 *
 \par
 *  LRU clean-up is performed through fqdncache_purgelru() according
 *  to the fqdncache_high threshold.
 */

/// \ingroup FQDNCacheInternal
#define FQDN_LOW_WATER       90

/// \ingroup FQDNCacheInternal
#define FQDN_HIGH_WATER      95

/**
 * The data structure used for storing name-address mappings
 * is a small hashtable (static hash_table *fqdn_table),
 * where structures of type fqdncache_entry whose most
 * interesting members are:
 */
class fqdncache_entry
{
    MEMPROXY_CLASS(fqdncache_entry);

public:
    fqdncache_entry(const char *name);
    ~fqdncache_entry();

    hash_link hash;     /* must be first */
    time_t lastref;
    time_t expires;
    unsigned char name_count;
    char *names[FQDN_MAX_NAMES + 1];
    FQDNH *handler;
    void *handlerData;
    char *error_message;

    struct timeval request_time;
    dlink_node lru;
    unsigned short locks;

    struct Flags {
        Flags() : negcached(false), fromhosts(false) {}

        bool negcached;
        bool fromhosts;
    } flags;

    int age() const; ///< time passed since request_time or -1 if unknown
};

/// \ingroup FQDNCacheInternal
static struct _fqdn_cache_stats {
    int requests;
    int replies;
    int hits;
    int misses;
    int negative_hits;
} FqdncacheStats;

/// \ingroup FQDNCacheInternal
static dlink_list lru_list;

static IDNSCB fqdncacheHandleReply;
static int fqdncacheParse(fqdncache_entry *, const rfc1035_rr *, int, const char *error_message);
static void fqdncacheRelease(fqdncache_entry *);
static void fqdncacheCallback(fqdncache_entry *, int wait);
static fqdncache_entry *fqdncache_get(const char *);
static int fqdncacheExpiredEntry(const fqdncache_entry *);
static void fqdncacheLockEntry(fqdncache_entry * f);
static void fqdncacheUnlockEntry(fqdncache_entry * f);
static void fqdncacheAddEntry(fqdncache_entry * f);

/// \ingroup FQDNCacheInternal
static hash_table *fqdn_table = nullptr;

/// \ingroup FQDNCacheInternal
static long fqdncache_low = 180;

/// \ingroup FQDNCacheInternal
static long fqdncache_high = 200;

/// \ingroup FQDNCacheInternal
inline int fqdncacheCount() { return fqdn_table ? fqdn_table->count : 0; }

int
fqdncache_entry::age() const
{
    return request_time.tv_sec ? tvSubMsec(request_time, current_time) : -1;
}

/**
 \ingroup FQDNCacheInternal
 * Removes the given fqdncache entry
 */
static void
fqdncacheRelease(fqdncache_entry * f)
{
    hash_remove_link(fqdn_table, (hash_link *) f);
    debugs(35, 5, "fqdncacheRelease: Released FQDN record for '" << hashKeyStr(&f->hash) << "'.");
    dlinkDelete(&f->lru, &lru_list);
    delete f;
}

/**
 \ingroup FQDNCacheInternal
 \param name    FQDN hash string.
 \retval Match for given name
 */
static fqdncache_entry *
fqdncache_get(const char *name)
{
    hash_link *e;
    static fqdncache_entry *f;
    f = nullptr;

    if (fqdn_table) {
        if ((e = (hash_link *)hash_lookup(fqdn_table, name)) != nullptr)
            f = (fqdncache_entry *) e;
    }

    return f;
}

/// \ingroup FQDNCacheInternal
static int
fqdncacheExpiredEntry(const fqdncache_entry * f)
{
    /* all static entries are locked, so this takes care of them too */

    if (f->locks != 0)
        return 0;

    if (f->expires > squid_curtime)
        return 0;

    return 1;
}

/// \ingroup FQDNCacheAPI
void
fqdncache_purgelru(void *)
{
    dlink_node *m;
    dlink_node *prev = nullptr;
    fqdncache_entry *f;
    int removed = 0;
    eventAdd("fqdncache_purgelru", fqdncache_purgelru, nullptr, 10.0, 1);

    for (m = lru_list.tail; m; m = prev) {
        if (fqdncacheCount() < fqdncache_low)
            break;

        prev = m->prev;

        f = (fqdncache_entry *)m->data;

        if (f->locks != 0)
            continue;

        fqdncacheRelease(f);

        ++removed;
    }

    debugs(35, 9, "fqdncache_purgelru: removed " << removed << " entries");
}

/// \ingroup FQDNCacheAPI
static void
purge_entries_fromhosts(void)
{
    dlink_node *m = lru_list.head;
    fqdncache_entry *i = nullptr;
    fqdncache_entry *t;

    while (m) {
        if (i != nullptr) {    /* need to delay deletion */
            fqdncacheRelease(i);    /* we just override locks */
            i = nullptr;
        }

        t = (fqdncache_entry *)m->data;

        if (t->flags.fromhosts)
            i = t;

        m = m->next;
    }

    if (i != nullptr)
        fqdncacheRelease(i);
}

fqdncache_entry::fqdncache_entry(const char *name) :
    lastref(0),
    expires(squid_curtime + Config.negativeDnsTtl),
    name_count(0),
    handler(nullptr),
    handlerData(nullptr),
    error_message(nullptr),
    locks(0) // XXX: use Lock
{
    hash.key = xstrdup(name);

    memset(&request_time, 0, sizeof(request_time));
    memset(&names, 0, sizeof(names));
}

/// \ingroup FQDNCacheInternal
static void
fqdncacheAddEntry(fqdncache_entry * f)
{
    hash_link *e = (hash_link *)hash_lookup(fqdn_table, f->hash.key);

    if (nullptr != e) {
        /* avoid collision */
        fqdncache_entry *q = (fqdncache_entry *) e;
        fqdncacheRelease(q);
    }

    hash_join(fqdn_table, &f->hash);
    dlinkAdd(f, &f->lru, &lru_list);
    f->lastref = squid_curtime;
}

/**
 \ingroup FQDNCacheInternal
 *
 * Walks down the pending list, calling handlers
 */
static void
fqdncacheCallback(fqdncache_entry * f, int wait)
{
    FQDNH *callback;
    void *cbdata;
    f->lastref = squid_curtime;

    if (!f->handler)
        return;

    fqdncacheLockEntry(f);

    callback = f->handler;

    f->handler = nullptr;

    if (cbdataReferenceValidDone(f->handlerData, &cbdata)) {
        const Dns::LookupDetails details(SBuf(f->error_message), wait);
        callback(f->name_count ? f->names[0] : nullptr, details, cbdata);
    }

    fqdncacheUnlockEntry(f);
}

/// \ingroup FQDNCacheInternal
static int
fqdncacheParse(fqdncache_entry *f, const rfc1035_rr * answers, int nr, const char *error_message)
{
    int k;
    int ttl = 0;
    const char *name = (const char *)f->hash.key;
    f->expires = squid_curtime + Config.negativeDnsTtl;
    f->flags.negcached = true;

    if (nr < 0) {
        debugs(35, 3, "fqdncacheParse: Lookup of '" << name << "' failed (" << error_message << ")");
        f->error_message = xstrdup(error_message);
        return -1;
    }

    if (nr == 0) {
        debugs(35, 3, "fqdncacheParse: No DNS records for '" << name << "'");
        f->error_message = xstrdup("No DNS records");
        return 0;
    }

    debugs(35, 3, "fqdncacheParse: " << nr << " answers for '" << name << "'");
    assert(answers);

    for (k = 0; k < nr; ++k) {
        if (answers[k]._class != RFC1035_CLASS_IN)
            continue;

        if (answers[k].type == RFC1035_TYPE_PTR) {
            if (!answers[k].rdata[0]) {
                debugs(35, 2, "fqdncacheParse: blank PTR record for '" << name << "'");
                continue;
            }

            if (strchr(answers[k].rdata, ' ')) {
                debugs(35, 2, "fqdncacheParse: invalid PTR record '" << answers[k].rdata << "' for '" << name << "'");
                continue;
            }

            f->names[f->name_count] = xstrdup(answers[k].rdata);
            ++ f->name_count;
        } else if (answers[k].type != RFC1035_TYPE_CNAME)
            continue;

        if (ttl == 0 || (int) answers[k].ttl < ttl)
            ttl = answers[k].ttl;

        if (f->name_count >= FQDN_MAX_NAMES)
            break;
    }

    if (f->name_count == 0) {
        debugs(35, DBG_IMPORTANT, "fqdncacheParse: No PTR record for '" << name << "'");
        return 0;
    }

    if (ttl > Config.positiveDnsTtl)
        ttl = Config.positiveDnsTtl;

    if (ttl < Config.negativeDnsTtl)
        ttl = Config.negativeDnsTtl;

    f->expires = squid_curtime + ttl;

    f->flags.negcached = false;

    return f->name_count;
}

/**
 \ingroup FQDNCacheAPI
 *
 * Callback for handling DNS results.
 */
static void
fqdncacheHandleReply(void *data, const rfc1035_rr * answers, int na, const char *error_message, const bool lastAnswer)
{
    assert(lastAnswer); // reverse DNS lookups do not generate multiple queries
    fqdncache_entry *f;
    static_cast<generic_cbdata *>(data)->unwrap(&f);
    ++FqdncacheStats.replies;
    const int age = f->age();
    statCounter.dns.svcTime.count(age);
    fqdncacheParse(f, answers, na, error_message);
    fqdncacheAddEntry(f);
    fqdncacheCallback(f, age);
}

/**
 \ingroup FQDNCacheAPI
 *
 \param addr        IP address of domain to resolve.
 \param handler     A pointer to the function to be called when
 *          the reply from the FQDN cache
 *          (or the DNS if the FQDN cache misses)
 \param handlerData Information that is passed to the handler
 *          and does not affect the FQDN cache.
 */
void
fqdncache_nbgethostbyaddr(const Ip::Address &addr, FQDNH * handler, void *handlerData)
{
    fqdncache_entry *f = nullptr;
    char name[MAX_IPSTRLEN];
    generic_cbdata *c;
    addr.toStr(name,MAX_IPSTRLEN);
    debugs(35, 4, "fqdncache_nbgethostbyaddr: Name '" << name << "'.");
    ++FqdncacheStats.requests;

    if (name[0] == '\0') {
        debugs(35, 4, "fqdncache_nbgethostbyaddr: Invalid name!");
        static const Dns::LookupDetails details(SBuf("Invalid hostname"), -1); // error, no lookup
        if (handler)
            handler(nullptr, details, handlerData);
        return;
    }

    f = fqdncache_get(name);

    if (nullptr == f) {
        /* miss */
        (void) 0;
    } else if (fqdncacheExpiredEntry(f)) {
        /* hit, but expired -- bummer */
        fqdncacheRelease(f);
        f = nullptr;
    } else {
        /* hit */
        debugs(35, 4, "fqdncache_nbgethostbyaddr: HIT for '" << name << "'");

        if (f->flags.negcached)
            ++ FqdncacheStats.negative_hits;
        else
            ++ FqdncacheStats.hits;

        f->handler = handler;

        f->handlerData = cbdataReference(handlerData);

        fqdncacheCallback(f, -1); // no lookup

        return;
    }

    debugs(35, 5, "fqdncache_nbgethostbyaddr: MISS for '" << name << "'");
    ++ FqdncacheStats.misses;
    f = new fqdncache_entry(name);
    f->handler = handler;
    f->handlerData = cbdataReference(handlerData);
    f->request_time = current_time;
    c = new generic_cbdata(f);
    idnsPTRLookup(addr, fqdncacheHandleReply, c);
}

/**
 \ingroup FQDNCacheAPI
 *
 * Is different in that it only checks if an entry exists in
 * it's data-structures and does not by default contact the
 * DNS, unless this is requested, by setting the flags
 * to FQDN_LOOKUP_IF_MISS.
 *
 \param addr    address of the FQDN being resolved
 \param flags   values are NULL or FQDN_LOOKUP_IF_MISS. default is NULL.
 *
 */
const char *
fqdncache_gethostbyaddr(const Ip::Address &addr, int flags)
{
    char name[MAX_IPSTRLEN];
    fqdncache_entry *f = nullptr;

    if (addr.isAnyAddr() || addr.isNoAddr()) {
        debugs(35, 7, "nothing to lookup: " << addr);
        return nullptr;
    }

    addr.toStr(name,MAX_IPSTRLEN);
    ++ FqdncacheStats.requests;
    f = fqdncache_get(name);

    if (nullptr == f) {
        (void) 0;
    } else if (fqdncacheExpiredEntry(f)) {
        fqdncacheRelease(f);
        f = nullptr;
    } else if (f->flags.negcached) {
        debugs(35, 5, "negative HIT: " << addr);
        ++ FqdncacheStats.negative_hits;
        // ignore f->error_message: the caller just checks FQDN cache presence
        return nullptr;
    } else {
        debugs(35, 5, "HIT: " << addr);
        ++ FqdncacheStats.hits;
        f->lastref = squid_curtime;
        // ignore f->error_message: the caller just checks FQDN cache presence
        return f->names[0];
    }

    /* no entry [any more] */
    debugs(35, 5, "MISS: " << addr);
    ++ FqdncacheStats.misses;

    if (flags & FQDN_LOOKUP_IF_MISS) {
        fqdncache_nbgethostbyaddr(addr, nullptr, nullptr);
    }

    return nullptr;
}

/**
 \ingroup FQDNCacheInternal
 *
 * Process objects list
 */
void
fqdnStats(StoreEntry * sentry)
{
    fqdncache_entry *f = nullptr;
    int k;
    int ttl;

    if (fqdn_table == nullptr)
        return;

    storeAppendPrintf(sentry, "FQDN Cache Statistics:\n");

    storeAppendPrintf(sentry, "FQDNcache Entries In Use: %d\n",
                      fqdncache_entry::UseCount());

    storeAppendPrintf(sentry, "FQDNcache Entries Cached: %d\n",
                      fqdncacheCount());

    storeAppendPrintf(sentry, "FQDNcache Requests: %d\n",
                      FqdncacheStats.requests);

    storeAppendPrintf(sentry, "FQDNcache Hits: %d\n",
                      FqdncacheStats.hits);

    storeAppendPrintf(sentry, "FQDNcache Negative Hits: %d\n",
                      FqdncacheStats.negative_hits);

    storeAppendPrintf(sentry, "FQDNcache Misses: %d\n",
                      FqdncacheStats.misses);

    storeAppendPrintf(sentry, "FQDN Cache Contents:\n\n");

    storeAppendPrintf(sentry, "%-45.45s %3s %3s %3s %s\n",
                      "Address", "Flg", "TTL", "Cnt", "Hostnames");

    hash_first(fqdn_table);

    while ((f = (fqdncache_entry *) hash_next(fqdn_table))) {
        ttl = (f->flags.fromhosts ? -1 : (f->expires - squid_curtime));
        storeAppendPrintf(sentry, "%-45.45s  %c%c %3.3d % 3d",
                          hashKeyStr(&f->hash),
                          f->flags.negcached ? 'N' : ' ',
                          f->flags.fromhosts ? 'H' : ' ',
                          ttl,
                          (int) f->name_count);

        for (k = 0; k < (int) f->name_count; ++k)
            storeAppendPrintf(sentry, " %s", f->names[k]);

        storeAppendPrintf(sentry, "\n");
    }
}

/// \ingroup FQDNCacheInternal
static void
fqdncacheLockEntry(fqdncache_entry * f)
{
    if (f->locks++ == 0) {
        dlinkDelete(&f->lru, &lru_list);
        dlinkAdd(f, &f->lru, &lru_list);
    }
}

/// \ingroup FQDNCacheInternal
static void
fqdncacheUnlockEntry(fqdncache_entry * f)
{
    assert(f->locks > 0);
    -- f->locks;

    if (fqdncacheExpiredEntry(f))
        fqdncacheRelease(f);
}

fqdncache_entry::~fqdncache_entry()
{
    for (int k = 0; k < (int)name_count; ++k)
        xfree(names[k]);

    xfree(hash.key);
    xfree(error_message);
}

/**
 \ingroup FQDNCacheAPI
 *
 * Recalculate FQDN cache size upon reconfigure.
 * Is called to clear the FQDN cache's data structures,
 * cancel all pending requests.
 */
void
fqdncache_restart(void)
{
    fqdncache_high = (long) (((float) Config.fqdncache.size *
                              (float) FQDN_HIGH_WATER) / (float) 100);
    fqdncache_low = (long) (((float) Config.fqdncache.size *
                             (float) FQDN_LOW_WATER) / (float) 100);
    purge_entries_fromhosts();
}

/**
 * Adds a "static" entry from /etc/hosts.
 *
 \param addr        FQDN name to be added.
 \param hostnames   list of hostnames for the addr
 */
void
fqdncacheAddEntryFromHosts(char *addr, SBufList &hostnames)
{
    fqdncache_entry *fce= fqdncache_get(addr);
    if (fce) {
        if (1 == fce->flags.fromhosts) {
            fqdncacheUnlockEntry(fce);
        } else if (fce->locks > 0) {
            debugs(35, DBG_IMPORTANT, "WARNING: can't add static entry for locked address '" << addr << "'");
            return;
        } else {
            fqdncacheRelease(fce);
        }
    }

    fce = new fqdncache_entry(addr);

    int j = 0;
    for (auto &h : hostnames) {
        fce->names[j] = xstrdup(h.c_str());
        Tolower(fce->names[j]);
        ++j;

        if (j >= FQDN_MAX_NAMES)
            break;
    }

    fce->name_count = j;
    fce->names[j] = nullptr;   /* it's safe */
    fce->flags.fromhosts = true;
    fqdncacheAddEntry(fce);
    fqdncacheLockEntry(fce);
}

/// \ingroup FQDNCacheInternal
static void
fqdncacheRegisterWithCacheManager(void)
{
    Mgr::RegisterAction("fqdncache", "FQDN Cache Stats and Contents",
                        fqdnStats, 0, 1);

}

/**
 \ingroup FQDNCacheAPI
 *
 * Initialize the fqdncache.
 * Called after IP cache initialization.
 */
void
fqdncache_init(void)
{
    int n;

    fqdncacheRegisterWithCacheManager();

    if (fqdn_table)
        return;

    debugs(35, 3, "Initializing FQDN Cache...");

    memset(&FqdncacheStats, '\0', sizeof(FqdncacheStats));
    lru_list = dlink_list();

    fqdncache_high = (long) (((float) Config.fqdncache.size *
                              (float) FQDN_HIGH_WATER) / (float) 100);

    fqdncache_low = (long) (((float) Config.fqdncache.size *
                             (float) FQDN_LOW_WATER) / (float) 100);

    n = hashPrime(fqdncache_high / 4);

    fqdn_table = hash_create((HASHCMP *) strcmp, n, hash4);
}

#if SQUID_SNMP
/**
 *  \ingroup FQDNCacheAPI
 * The function to return the FQDN statistics via SNMP
 */
variable_list *
snmp_netFqdnFn(variable_list * Var, snint * ErrP)
{
    variable_list *Answer = nullptr;
    MemBuf tmp;
    debugs(49, 5, "snmp_netFqdnFn: Processing request:" << snmpDebugOid(Var->name, Var->name_length, tmp));
    *ErrP = SNMP_ERR_NOERROR;

    switch (Var->name[LEN_SQ_NET + 1]) {

    case FQDN_ENT:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      fqdncacheCount(),
                                      SMI_GAUGE32);
        break;

    case FQDN_REQ:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      FqdncacheStats.requests,
                                      SMI_COUNTER32);
        break;

    case FQDN_HITS:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      FqdncacheStats.hits,
                                      SMI_COUNTER32);
        break;

    case FQDN_PENDHIT:
        /* this is now worthless */
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      0,
                                      SMI_GAUGE32);
        break;

    case FQDN_NEGHIT:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      FqdncacheStats.negative_hits,
                                      SMI_COUNTER32);
        break;

    case FQDN_MISS:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      FqdncacheStats.misses,
                                      SMI_COUNTER32);
        break;

    case FQDN_GHBN:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      0, /* deprecated */
                                      SMI_COUNTER32);
        break;

    default:
        *ErrP = SNMP_ERR_NOSUCHNAME;
        break;
    }

    return Answer;
}

#endif /*SQUID_SNMP */

