/*
 *  Copyright (C) 2013-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 *  Copyright (C) 2007-2013 Sourcefire, Inc.
 *  Copyright (C) 2002-2007 Tomasz Kojm <tkojm@clamav.net>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 */

#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <sys/types.h>
#include <dirent.h>
#ifndef _WIN32
#include <sys/wait.h>
#endif
#include <sys/stat.h>
#include <fcntl.h>
#ifdef HAVE_PWD_H
#include <pwd.h>
#endif
#ifdef HAVE_GRP_H
#include <grp.h>
#endif

#if defined(USE_SYSLOG) && !defined(C_AIX)
#include <syslog.h>
#endif

#include <curl/curl.h>

#include "target.h"

// libclamav
#include "clamav.h"
#include "others.h"
#include "regex_list.h"
#include "str.h"

// shared
#include "cert_util.h"
#include "output.h"
#include "misc.h"

#include "libfreshclam.h"
#include "libfreshclam_internal.h"
#include "dns.h"

/*
 * Private functions
 */

/*
 * libclamav API functions
 */
const char *fc_strerror(fc_error_t fcerror)
{
    switch (fcerror) {
        case FC_SUCCESS:
            return "Success";
        case FC_UPTODATE:
            return "Up-to-date";
        case FC_EINIT:
            return "Failed to initalize";
        case FC_EDIRECTORY:
            return "Invalid, nonexistant, or inaccessible directory";
        case FC_EFILE:
            return "Invalid, nonexistant, or inaccessible file";
        case FC_ECONNECTION:
            return "Connection failed";
        case FC_EEMPTYFILE:
            return "Empty file";
        case FC_EBADCVD:
            return "Invalid or corrupted CVD/CLD database";
        case FC_ETESTFAIL:
            return "Test failed";
        case FC_ECONFIG:
            return "Invalid configuration settings(s)";
        case FC_EDBDIRACCESS:
            return "Failed to read/write file to database directory";
        case FC_EFAILEDGET:
            return "HTTP GET failed";
        case FC_EMIRRORNOTSYNC:
            return "Downloaded database had lower version than advertised";
        case FC_ELOGGING:
            return "Failed to write to log";
        case FC_EFAILEDUPDATE:
            return "Failed to update database";
        case FC_EMEM:
            return "Memory allocation error";
        case FC_EARG:
            return "Invalid argument(s)";
        case FC_EFORBIDDEN:
            return "Forbidden; Blocked by CDN";
        case FC_ERETRYLATER:
            return "Too many requests; Retry later";
        default:
            return "Unknown libfreshclam error code!";
    }
}

fc_error_t fc_initialize(fc_config *fcConfig)
{
    fc_error_t status = FC_EARG;
    STATBUF statbuf;

    if (NULL == fcConfig) {
        printf("fc_initialize: Invalid arguments.\n");
        return status;
    }

    /* Initilize libcurl */
    curl_global_init(CURL_GLOBAL_ALL);

    /* Initialize mprintf options */
    if (fcConfig->msgFlags & FC_CONFIG_MSG_DEBUG) cl_debug();
    mprintf_verbose  = (fcConfig->msgFlags & FC_CONFIG_MSG_VERBOSE) ? 1 : 0;
    mprintf_quiet    = (fcConfig->msgFlags & FC_CONFIG_MSG_QUIET) ? 1 : 0;
    mprintf_nowarn   = (fcConfig->msgFlags & FC_CONFIG_MSG_NOWARN) ? 1 : 0;
    mprintf_stdout   = (fcConfig->msgFlags & FC_CONFIG_MSG_STDOUT) ? 1 : 0;
    mprintf_progress = (fcConfig->msgFlags & FC_CONFIG_MSG_SHOWPROGRESS) ? 1 : 0;

    /* Initialize logger */
    logg_verbose = (fcConfig->logFlags & FC_CONFIG_LOG_VERBOSE) ? 1 : 0;
    logg_nowarn  = (fcConfig->logFlags & FC_CONFIG_LOG_NOWARN) ? 1 : 0;
    logg_time    = (fcConfig->logFlags & FC_CONFIG_LOG_TIME) ? 1 : 0;
    logg_rotate  = (fcConfig->logFlags & FC_CONFIG_LOG_ROTATE) ? 1 : 0;
    logg_size    = fcConfig->maxLogSize;
    /* Set a log file if requested, and is not already set */
    if ((NULL == logg_file) && (NULL != fcConfig->logFile)) {
        logg_file = cli_strdup(fcConfig->logFile);
        if (0 != logg("#--------------------------------------\n")) {
            mprintf("!Problem with internal logger (UpdateLogFile = %s).\n", logg_file);
            status = FC_ELOGGING;
            goto done;
        }
    }

#if defined(USE_SYSLOG) && !defined(C_AIX)
    /* Initialize syslog if available and requested, and is not already set */
    if (fcConfig->logFlags & FC_CONFIG_LOG_SYSLOG) {
        int logFacility = LOG_LOCAL6;
        if ((0 == logg_syslog) && (NULL != fcConfig->logFacility) && (-1 == (logFacility = logg_facility(fcConfig->logFacility)))) {
            mprintf("!LogFacility: %s: No such facility.\n", fcConfig->logFacility);
            status = FC_ELOGGING;
            goto done;
        }

        openlog("freshclam", LOG_PID, logFacility);
        logg_syslog = 1;
    }
#endif

    /* Optional connection settings. */
    if (NULL != fcConfig->localIP) {
#if !((LIBCURL_VERSION_MAJOR > 7) || ((LIBCURL_VERSION_MAJOR == 7) && (LIBCURL_VERSION_MINOR >= 33)))
        mprintf("!The LocalIP feature was requested but this local IP support is not presently available.\n");
        mprintf("!Your installation was built with libcurl version %u.%u.%u.\n", LIBCURL_VERSION_MAJOR, LIBCURL_VERSION_MINOR, LIBCURL_VERSION_PATCH);
        mprintf("!LocalIP requires libcurl version 7.33.0 or higher and must include the c-ares optional dependency.\n");
#else
        g_localIP = cli_strdup(fcConfig->localIP);
#endif
    }
    if (NULL != fcConfig->userAgent) {
        g_userAgent = cli_strdup(fcConfig->userAgent);
    }
    if (NULL != fcConfig->proxyServer) {
        g_proxyServer = cli_strdup(fcConfig->proxyServer);
        if (0 != fcConfig->proxyPort) {
            g_proxyPort = fcConfig->proxyPort;
        } else {
            /*
             * Proxy port not provided. Look up the default port for
             * webcache in /etc/services.
             * Default to 8080 if not provided.
             */
            const struct servent *webcache = getservbyname("webcache", "TCP");

            if (webcache)
                g_proxyPort = ntohs(webcache->s_port);
            else
                g_proxyPort = 8080;

            endservent();
        }
    }
    if (NULL != fcConfig->proxyUsername) {
        g_proxyUsername = cli_strdup(fcConfig->proxyUsername);
    }
    if (NULL != fcConfig->proxyPassword) {
        g_proxyPassword = cli_strdup(fcConfig->proxyPassword);
    }

#ifdef _WIN32
    if ((fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '/') &&
        ((fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '\\'))) {
#else
    if (fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '/') {
#endif
        g_databaseDirectory = cli_malloc(strlen(fcConfig->databaseDirectory) + strlen(PATHSEP) + 1);
        snprintf(
            g_databaseDirectory,
            strlen(fcConfig->databaseDirectory) + strlen(PATHSEP) + 1,
            "%s" PATHSEP,
            fcConfig->databaseDirectory);
    } else {
        g_databaseDirectory = cli_strdup(fcConfig->databaseDirectory);
    }

    /* Validate that the database directory exists, and store it. */
    if (LSTAT(g_databaseDirectory, &statbuf) == -1) {
        logg("!Database directory does not exist: %s\n", g_databaseDirectory);
        status = FC_EDIRECTORY;
        goto done;
    }
    if (!S_ISDIR(statbuf.st_mode)) {
        logg("!Database directory is not a directory: %s\n", g_databaseDirectory);
        status = FC_EDIRECTORY;
        goto done;
    }

    g_tempDirectory = cli_strdup(fcConfig->tempDirectory);

    g_maxAttempts    = fcConfig->maxAttempts;
    g_connectTimeout = fcConfig->connectTimeout;
    g_requestTimeout = fcConfig->requestTimeout;

    g_bCompressLocalDatabase = fcConfig->bCompressLocalDatabase;

    /* Load or create freshclam.dat */
    if (FC_SUCCESS != load_freshclam_dat()) {
        logg("*Failed to load freshclam.dat; will create a new freshclam.dat\n");

        if (FC_SUCCESS != new_freshclam_dat()) {
            logg("^Failed to create a new freshclam.dat!\n");
            status = FC_EINIT;
            goto done;
        }
    }

    status = FC_SUCCESS;

done:
    if (FC_SUCCESS != status) {
        fc_cleanup();
    }

    return status;
}

void fc_cleanup(void)
{
    /* Cleanup libcurl */
    curl_global_cleanup();

    if (NULL != logg_file) {
        free((void *)logg_file);
        logg_file = NULL;
    }
    if (NULL != g_localIP) {
        free(g_localIP);
        g_localIP = NULL;
    }
    if (NULL != g_userAgent) {
        free(g_userAgent);
        g_userAgent = NULL;
    }
    if (NULL != g_proxyServer) {
        free(g_proxyServer);
        g_proxyServer = NULL;
    }
    if (NULL != g_proxyUsername) {
        free(g_proxyUsername);
        g_proxyUsername = NULL;
    }
    if (NULL != g_proxyPassword) {
        free(g_proxyPassword);
        g_proxyPassword = NULL;
    }
    if (NULL != g_databaseDirectory) {
        free(g_databaseDirectory);
        g_databaseDirectory = NULL;
    }
    if (NULL != g_tempDirectory) {
        free(g_tempDirectory);
        g_tempDirectory = NULL;
    }
    if (NULL != g_freshclamDat) {
        free(g_freshclamDat);
        g_freshclamDat = NULL;
    }
}

fc_error_t fc_prune_database_directory(char **databaseList, uint32_t nDatabases)
{
    fc_error_t status = FC_EARG;

    DIR *dir = NULL;
    struct dirent *dent;
    char *extension = NULL;

    /* Change directory to database directory */
    if (chdir(g_databaseDirectory)) {
        logg("!Can't change dir to %s\n", g_databaseDirectory);
        status = FC_EDIRECTORY;
        goto done;
    }

    logg("*Current working dir is %s\n", g_databaseDirectory);

    if (!(dir = opendir(g_databaseDirectory))) {
        logg("!checkdbdir: Can't open directory %s\n", g_databaseDirectory);
        status = FC_EDBDIRACCESS;
        goto done;
    }

    while ((dent = readdir(dir))) {
        if (dent->d_ino) {
            if ((NULL != (extension = strstr(dent->d_name, ".cld"))) ||
                (NULL != (extension = strstr(dent->d_name, ".cvd")))) {

                uint32_t i;
                int bFound = 0;
                for (i = 0; i < nDatabases; i++) {
                    if (0 == strncmp(databaseList[i], dent->d_name, extension - dent->d_name)) {
                        bFound = 1;
                    }
                }
                if (!bFound) {
                    /* Prune CVD/CLD */
                    mprintf("Pruning unwanted or deprecated database file %s.\n", dent->d_name);
                    if (unlink(dent->d_name)) {
                        mprintf("!Failed to prune unwanted database file %s, consider removing it manually.\n", dent->d_name);
                        status = FC_EDBDIRACCESS;
                        goto done;
                    }
                }
            }
        }
    }

    status = FC_SUCCESS;

done:
    if (NULL != dir) {
        closedir(dir);
    }

    return status;
}

/**
 * @brief Compare two version strings.
 *
 * @param v1 Version string 1
 * @param v2 Version string 2
 * @return int 1 if v1 is greater, 0 if equal, -1 if smaller.
 */
int version_string_compare(char *v1, size_t v1_len, char *v2, size_t v2_len)
{
    size_t i, j;
    int vnum1 = 0, vnum2 = 0;

    for (i = 0, j = 0; (i < v1_len || j < v2_len);) {
        while (i < v1_len && v1[i] != '.') {
            vnum1 = vnum1 * 10 + (v1[i] - '0');
            i++;
        }

        while (j < v2_len && v2[j] != '.') {
            vnum2 = vnum2 * 10 + (v2[j] - '0');
            j++;
        }

        if (vnum1 > vnum2)
            return 1;
        if (vnum2 > vnum1)
            return -1;

        vnum1 = vnum2 = 0;
        i++;
        j++;
    }
    return 0;
}

fc_error_t fc_test_database(const char *dbFilename, int bBytecodeEnabled)
{
    fc_error_t status        = FC_EARG;
    struct cl_engine *engine = NULL;
    unsigned newsigs         = 0;
    cl_error_t cl_ret;

    if ((NULL == dbFilename)) {
        logg("^fc_test_database: Invalid arguments.\n");
        goto done;
    }

    logg("*Loading signatures from %s\n", dbFilename);
    if (NULL == (engine = cl_engine_new())) {
        status = FC_ETESTFAIL;
        goto done;
    }

    cl_engine_set_clcb_stats_submit(engine, NULL);

    if (CL_SUCCESS != (cl_ret = cl_load(
                           dbFilename, engine, &newsigs,
                           CL_DB_PHISHING | CL_DB_PHISHING_URLS | CL_DB_BYTECODE |
                               CL_DB_PUA | CL_DB_ENHANCED))) {
        logg("!Failed to load new database: %s\n", cl_strerror(cl_ret));
        status = FC_ETESTFAIL;
        goto done;
    }

    if (bBytecodeEnabled && (CL_SUCCESS != (cl_ret = cli_bytecode_prepare2(
                                                engine, &engine->bcs,
                                                engine->dconf->bytecode
                                                /*FIXME: dconf has no sense here */)))) {
        logg("!Failed to compile/load bytecode: %s\n", cl_strerror(cl_ret));
        status = FC_ETESTFAIL;
        goto done;
    }
    logg("*Properly loaded %u signatures from %s\n", newsigs, dbFilename);

    status = FC_SUCCESS;

done:

    if (NULL != engine) {
        if (engine->domainlist_matcher && engine->domainlist_matcher->sha256_pfx_set.keys)
            cli_hashset_destroy(&engine->domainlist_matcher->sha256_pfx_set);

        cl_engine_free(engine);
    }

    return status;
}

fc_error_t fc_dns_query_update_info(
    const char *dnsUpdateInfoServer,
    char **dnsUpdateInfo,
    char **newVersion)
{
    fc_error_t status = FC_EFAILEDGET;
    char *dnsReply    = NULL;

#ifdef HAVE_RESOLV_H
    unsigned int ttl;
    char *reply_token = NULL;
    int recordTime;
    time_t currentTime;
    int vwarning = 1;
    char version_string[32];
#endif /* HAVE_RESOLV_H */

    if ((NULL == dnsUpdateInfo) || (NULL == newVersion)) {
        logg("^dns_query_update_info: Invalid arguments.\n");
        status = FC_EARG;
        goto done;
    }

    *dnsUpdateInfo = NULL;
    *newVersion    = NULL;

#ifdef HAVE_RESOLV_H

    if (dnsUpdateInfoServer == NULL) {
        logg("^DNS Update Info disabled. Falling back to HTTP mode.\n");
        goto done;
    }

    if (NULL == (dnsReply = dnsquery(dnsUpdateInfoServer, T_TXT, &ttl))) {
        logg("^Invalid DNS reply. Falling back to HTTP mode.\n");
        goto done;
    }

    logg("*TTL: %d\n", ttl);

    /*
     * Check Record Time.
     */
    if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_RECORDTIME, ":"))) {
        logg("^Failed to find Record Time field in DNS Update Info.\n");
        goto done;
    }

    recordTime = atoi(reply_token);
    free(reply_token);
    reply_token = NULL;

    time(&currentTime);
    if ((int)currentTime - recordTime > 10800) {
        logg("^DNS record is older than 3 hours.\n");
        goto done;
    }

    /*
     * Check Version Warning Flag.
     */
    if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_VERSIONWARNING, ":"))) {
        logg("^Failed to find Version Warning Flag in DNS Update Info.\n");
        goto done;
    }

    if (*reply_token == '0')
        vwarning = 0;
    free(reply_token);
    reply_token = NULL;

    /*
     * Check the latest available ClamAV software version.
     */
    if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_NEWVERSION, ":"))) {
        logg("^Failed to find New Version field in DNS Update Info.\n");
        goto done;
    }

    logg("*fc_dns_query_update_info: Software version from DNS: %s\n", reply_token);

    /*
     * Compare the latest available ClamAV version with this ClamAV version.
     * Only throw a warning if the Version Warning Flag was set,
     * and this is not a beta, release candidate, or development version.
     */
    strncpy(version_string, get_version(), sizeof(version_string));
    version_string[31] = 0;

    if (vwarning) {
        if (!strstr(version_string, "devel") &&
            !strstr(version_string, "beta") &&
            !strstr(version_string, "rc")) {

            char *suffix = strchr(version_string, '-');

            if ((suffix && (0 > version_string_compare(version_string, suffix - version_string, reply_token, strlen(reply_token)))) ||
                (!suffix && (0 > version_string_compare(version_string, strlen(version_string), reply_token, strlen(reply_token))))) {

                logg("^Your ClamAV installation is OUTDATED!\n");
                logg("^Local version: %s Recommended version: %s\n", version_string, reply_token);
                logg("DON'T PANIC! Read https://docs.clamav.net/manual/Installing.html\n");
                *newVersion = cli_strdup(reply_token);
            }
        }
    }

    free(reply_token);
    reply_token = NULL;

    *dnsUpdateInfo = dnsReply;

    status = FC_SUCCESS;

#endif /* HAVE_RESOLV_H */

done:

    if (FC_SUCCESS != status) {
        free(dnsReply);
    }

    return status;
}

fc_error_t fc_update_database(
    const char *database,
    char **serverList,
    uint32_t nServers,
    int bPrivateMirror,
    const char *dnsUpdateInfo,
    int bScriptedUpdates,
    void *context,
    int *bUpdated)
{
    fc_error_t ret;
    fc_error_t status = FC_EARG;

    char *dbFilename = NULL;
    int signo        = 0;
    long attempt     = 1;
    uint32_t i;

    if ((NULL == database) || (NULL == serverList) || (NULL == bUpdated)) {
        logg("^fc_update_database: Invalid arguments.\n");
        goto done;
    }

    *bUpdated = 0;

    /* Change directory to database directory */
    if (chdir(g_databaseDirectory)) {
        logg("!Can't change dir to %s\n", g_databaseDirectory);
        status = FC_EDIRECTORY;
        goto done;
    }
    logg("*Current working dir is %s\n", g_databaseDirectory);

    /*
     * Attempt to update official database using DatabaseMirrors or PrivateMirrors.
     */
    for (i = 0; i < nServers; i++) {
        for (attempt = 1; attempt <= g_maxAttempts; attempt++) {
            ret = updatedb(
                database,
                dnsUpdateInfo,
                serverList[i],
                bPrivateMirror,
                context,
                bScriptedUpdates,
                attempt == g_maxAttempts ? 1 : 0,
                &signo,
                &dbFilename,
                bUpdated);

            switch (ret) {
                case FC_SUCCESS: {
                    if (*bUpdated) {
                        logg("*fc_update_database: %s updated.\n", dbFilename);
                    } else {
                        logg("*fc_update_database: %s already up-to-date.\n", dbFilename);
                    }
                    goto success;
                }
                case FC_ECONNECTION:
                case FC_EBADCVD:
                case FC_EFAILEDGET: {
                    if (attempt < g_maxAttempts) {
                        logg("Trying again in 5 secs...\n");
                        sleep(5);
                    } else {
                        logg("Giving up on %s...\n", serverList[i]);
                        if (i == nServers - 1) {
                            logg("!Update failed for database: %s\n", database);
                            status = ret;
                            goto done;
                        }
                    }
                    break;
                }
                case FC_EMIRRORNOTSYNC: {
                    logg("Received an older %s CVD than was advertised. We'll retry so the incremental update will ensure we're up-to-date.\n", database);
                    break;
                }
                case FC_EFORBIDDEN: {
                    char retry_after_string[26];
                    struct tm *tm_info;
                    tm_info = localtime(&g_freshclamDat->retry_after);
                    if (NULL == tm_info) {
                        logg("!Failed to query the local time for the retry-after date!\n");
                        status = FC_ERROR;
                        goto done;
                    }
                    strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
                    logg("^FreshClam received error code 403 from the ClamAV Content Delivery Network (CDN).\n");
                    logg("This could mean several things:\n");
                    logg(" 1. You are running an out-of-date version of ClamAV / FreshClam.\n");
                    logg("    Ensure you are the most updated version by visiting https://www.clamav.net/downloads\n");
                    logg(" 2. Your network is explicitly denied by the FreshClam CDN.\n");
                    logg("    In order to rectify this please check that you are:\n");
                    logg("   a. Running an up-to-date version of FreshClam\n");
                    logg("   b. Running FreshClam no more than once an hour\n");
                    logg("   c. If you have checked (a) and (b), please open a ticket at\n");
                    logg("      https://github.com/Cisco-Talos/clamav/issues\n");
                    logg("      and we will investigate why your network is blocked.\n");
                    logg("^You are on cool-down until after: %s\n", retry_after_string);
                    status = ret;
                    goto done;
                    break;
                }
                case FC_ERETRYLATER: {
                    char retry_after_string[26];
                    struct tm *tm_info;
                    tm_info = localtime(&g_freshclamDat->retry_after);
                    if (NULL == tm_info) {
                        logg("!Failed to query the local time for the retry-after date!\n");
                        status = FC_ERROR;
                        goto done;
                    }
                    strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
                    logg("^FreshClam received error code 429 from the ClamAV Content Delivery Network (CDN).\n");
                    logg("This means that you have been rate limited by the CDN.\n");
                    logg(" 1. Run FreshClam no more than once an hour to check for updates.\n");
                    logg("    FreshClam should check DNS first to see if an update is needed.\n");
                    logg(" 2. If you have more than 10 hosts on your network attempting to download,\n");
                    logg("    it is recommended that you set up a private mirror on your network using\n");
                    logg("    cvdupdate (https://pypi.org/project/cvdupdate/) to save bandwidth on the\n");
                    logg("    CDN and your own network.\n");
                    logg(" 3. Please do not open a ticket asking for an exemption from the rate limit,\n");
                    logg("    it will not be granted.\n");
                    logg("^You are on cool-down until after: %s\n", retry_after_string);
                    goto success;
                    break;
                }
                default: {
                    logg("!Unexpected error when attempting to update %s: %s\n", database, fc_strerror(ret));
                    status = ret;
                    goto done;
                }
            }
        }
    }

success:

    status = FC_SUCCESS;

done:

    if (NULL != dbFilename) {
        free(dbFilename);
    }

    return status;
}

fc_error_t fc_update_databases(
    char **databaseList,
    uint32_t nDatabases,
    char **serverList,
    uint32_t nServers,
    int bPrivateMirror,
    const char *dnsUpdateInfo,
    int bScriptedUpdates,
    void *context,
    uint32_t *nUpdated)
{
    fc_error_t ret;
    fc_error_t status = FC_EARG;
    uint32_t i;
    int bUpdated        = 0;
    uint32_t numUpdated = 0;

    if ((NULL == databaseList) || (0 == nDatabases) || (NULL == serverList) || (NULL == nUpdated)) {
        logg("^fc_update_databases: Invalid arguments.\n");
        goto done;
    }

    *nUpdated = 0;

    if (g_freshclamDat->retry_after > 0) {
        if (g_freshclamDat->retry_after > time(NULL)) {
            /* We're on cool-down, try again later. */
            char retry_after_string[26];
            struct tm *tm_info;
            tm_info = localtime(&g_freshclamDat->retry_after);
            if (NULL == tm_info) {
                logg("!Failed to query the local time for the retry-after date!\n");
                status = FC_ERROR;
                goto done;
            }
            strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
            logg("^FreshClam previously received error code 429 or 403 from the ClamAV Content Delivery Network (CDN).\n");
            logg("This means that you have been rate limited or blocked by the CDN.\n");
            logg(" 1. Verify that you're running a supported ClamAV version.\n");
            logg("    See https://docs.clamav.net/faq/faq-eol.html for details.\n");
            logg(" 2. Run FreshClam no more than once an hour to check for updates.\n");
            logg("    FreshClam should check DNS first to see if an update is needed.\n");
            logg(" 3. If you have more than 10 hosts on your network attempting to download,\n");
            logg("    it is recommended that you set up a private mirror on your network using\n");
            logg("    cvdupdate (https://pypi.org/project/cvdupdate/) to save bandwidth on the\n");
            logg("    CDN and your own network.\n");
            logg(" 4. Please do not open a ticket asking for an exemption from the rate limit,\n");
            logg("    it will not be granted.\n");
            logg("^You are still on cool-down until after: %s\n", retry_after_string);
            status = FC_SUCCESS;
            goto done;
        } else {
            g_freshclamDat->retry_after = 0;
            logg("^Cool-down expired, ok to try again.\n");
            save_freshclam_dat();
        }
    }

    for (i = 0; i < nDatabases; i++) {
        if (FC_SUCCESS != (ret = fc_update_database(
                               databaseList[i],
                               serverList,
                               nServers,
                               bPrivateMirror,
                               dnsUpdateInfo,
                               bScriptedUpdates,
                               context,
                               &bUpdated))) {
            status = ret;
            goto done;
        }
        if (bUpdated) {
            numUpdated++;
        }
    }

    *nUpdated = numUpdated;
    status    = FC_SUCCESS;

done:

    return status;
}

fc_error_t fc_download_url_database(
    const char *urlDatabase,
    void *context,
    int *bUpdated)
{
    fc_error_t ret;
    fc_error_t status = FC_EARG;

    long attempt     = 1;
    char *dbFilename = NULL;

    if ((NULL == urlDatabase) || (NULL == bUpdated)) {
        logg("^fc_download_url_database: Invalid arguments.\n");
        goto done;
    }

    *bUpdated = 0;

    /* Change directory to database directory */
    if (chdir(g_databaseDirectory)) {
        logg("!Can't change dir to %s\n", g_databaseDirectory);
        status = FC_EDIRECTORY;
        goto done;
    }
    logg("*Current working dir is %s\n", g_databaseDirectory);

    /*
     * Attempt to update official database using DatabaseMirrors or PrivateMirrors.
     */
    for (attempt = 1; attempt <= g_maxAttempts; attempt++) {
        int signo = 0;

        ret = updatecustomdb(
            urlDatabase,
            context,
            attempt == g_maxAttempts ? 1 : 0,
            &signo,
            &dbFilename,
            bUpdated);

        switch (ret) {
            case FC_SUCCESS: {
                if (*bUpdated) {
                    logg("*fc_download_url_database: %s updated.\n", dbFilename);
                } else {
                    logg("*fc_download_url_database: %s already up-to-date.\n", dbFilename);
                }
                goto success;
            }
            case FC_ECONNECTION:
            case FC_EBADCVD:
            case FC_EFAILEDGET: {
                if (attempt < g_maxAttempts) {
                    logg("Trying again in 5 secs...\n");
                    sleep(5);
                } else {
                    logg("Update failed for custom database URL: %s\n", urlDatabase);
                    status = ret;
                    goto done;
                }
                break;
            }
            case FC_EFORBIDDEN: {
                char retry_after_string[26];
                struct tm *tm_info;
                tm_info = localtime(&g_freshclamDat->retry_after);
                if (NULL == tm_info) {
                    logg("!Failed to query the local time for the retry-after date!\n");
                    status = FC_ERROR;
                    goto done;
                }
                strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
                logg("^FreshClam received error code 403 from the ClamAV Content Delivery Network (CDN).\n");
                logg("This could mean several things:\n");
                logg(" 1. You are running an out-of-date version of ClamAV / FreshClam.\n");
                logg("    Ensure you are the most updated version by visiting https://www.clamav.net/downloads\n");
                logg(" 2. Your network is explicitly denied by the FreshClam CDN.\n");
                logg("    In order to rectify this please check that you are:\n");
                logg("   a. Running an up-to-date version of FreshClam\n");
                logg("   b. Running FreshClam no more than once an hour\n");
                logg("   c. If you have checked (a) and (b), please open a ticket at\n");
                logg("      https://github.com/Cisco-Talos/clamav/issues\n");
                logg("      and we will investigate why your network is blocked.\n");
                logg("^You are on cool-down until after: %s\n", retry_after_string);
                status = ret;
                goto done;
                break;
            }
            case FC_ERETRYLATER: {
                char retry_after_string[26];
                struct tm *tm_info;
                tm_info = localtime(&g_freshclamDat->retry_after);
                if (NULL == tm_info) {
                    logg("!Failed to query the local time for the retry-after date!\n");
                    status = FC_ERROR;
                    goto done;
                }
                strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
                logg("^FreshClam received error code 429 from the ClamAV Content Delivery Network (CDN).\n");
                logg("This means that you have been rate limited by the CDN.\n");
                logg(" 1. Run FreshClam no more than once an hour to check for updates.\n");
                logg("    FreshClam should check DNS first to see if an update is needed.\n");
                logg(" 2. If you have more than 10 hosts on your network attempting to download,\n");
                logg("    it is recommended that you set up a private mirror on your network using\n");
                logg("    cvdupdate (https://pypi.org/project/cvdupdate/) to save bandwidth on the\n");
                logg("    CDN and your own network.\n");
                logg(" 3. Please do not open a ticket asking for an exemption from the rate limit,\n");
                logg("    it will not be granted.\n");
                logg("^You are on cool-down until after: %s\n", retry_after_string);
                goto success;
                break;
            }
            default: {
                logg("Unexpected error when attempting to update from custom database URL: %s\n", urlDatabase);
                status = ret;
                goto done;
            }
        }
    }

success:

    status = FC_SUCCESS;

done:

    if (NULL != dbFilename) {
        free(dbFilename);
    }

    return status;
}

fc_error_t fc_download_url_databases(
    char **urlDatabaseList,
    uint32_t nUrlDatabases,
    void *context,
    uint32_t *nUpdated)
{
    fc_error_t ret;
    fc_error_t status   = FC_EARG;
    int bUpdated        = 0;
    uint32_t numUpdated = 0;
    uint32_t i;

    if ((NULL == urlDatabaseList) || (0 == nUrlDatabases) || (NULL == nUpdated)) {
        logg("^fc_download_url_databases: Invalid arguments.\n");
        goto done;
    }

    *nUpdated = 0;

    for (i = 0; i < nUrlDatabases; i++) {
        if (FC_SUCCESS != (ret = fc_download_url_database(
                               urlDatabaseList[i],
                               context,
                               &bUpdated))) {
            logg("^fc_download_url_databases: fc_download_url_database failed: %s (%d)\n", fc_strerror(ret), ret);
            status = ret;
            goto done;
        }
        if (bUpdated) {
            numUpdated++;
        }
    }

    *nUpdated = numUpdated;
    status    = FC_SUCCESS;

done:

    return status;
}

void fc_set_fccb_download_complete(fccb_download_complete callback)
{
    g_cb_download_complete = callback;
}
