/* $Id: ncbi_localnet.c 666380 2023-04-25 15:27:13Z lavr $
 * ===========================================================================
 *
 *                            PUBLIC DOMAIN NOTICE
 *               National Center for Biotechnology Information
 *
 *  This software/database is a "United States Government Work" under the
 *  terms of the United States Copyright Act.  It was written as part of
 *  the author's official duties as a United States Government employee and
 *  thus cannot be copyrighted.  This software/database is freely available
 *  to the public for use. The National Library of Medicine and the U.S.
 *  Government have not placed any restriction on its use or reproduction.
 *
 *  Although all reasonable efforts have been taken to ensure the accuracy
 *  and reliability of the software and data, the NLM and the U.S.
 *  Government do not and cannot warrant the performance or results that
 *  may be obtained by using this software or data. The NLM and the U.S.
 *  Government disclaim all warranties, express or implied, including
 *  warranties of performance, merchantability or fitness for any particular
 *  purpose.
 *
 *  Please cite the author in any work or product based on this material.
 *
 * ===========================================================================
 *
 * Author:  Anton Lavrentiev
 *
 * File Description:
 *   Get IP address of CGI client and determine the IP locality
 *
 */

#include "../ncbi_ansi_ext.h"
#include "../ncbi_priv.h"
#include <connect/ncbi_socket.h>
#include <connect/ext/ncbi_localnet.h>
#include <stdlib.h>
#if defined(NCBI_OS_UNIX)
#  include <sys/types.h>
#  include <netinet/in.h>
#endif /*NCBI_OS_UNIX*/


#ifndef   INADDR_LOOPBACK
#  define INADDR_LOOPBACK  0x7F000001
#endif /*!INADDR_LOOPBACK*/

#ifdef    SizeOf
#  undef  SizeOf
#endif  /*SizeOf*/
#define   SizeOf(a)  (sizeof(a) / sizeof((a)[0]))

#if defined(_DEBUG)  &&  !defined(NDEBUG)
/*#  define NCBI_LOCALNET_DEBUG  1*/
#endif /*_DEBUG && !NDEBUG*/


#ifdef __GNUC__
inline
#endif /*__GNUC__*/
static int/*bool*/ x_IsPrivateIP(unsigned int addr)
{
    return
#if   defined(IN_LOOPBACK)
        IN_LOOPBACK(addr)  ||
#elif defined(IN_LOOPBACKNET) &&                                        \
    defined(IN_CLASSA) && defined(IN_CLASSA_NET) && defined(IN_CLASSA_NSHIFT)
        (IN_CLASSA(addr)
         && (addr & IN_CLASSA_NET) == (IN_LOOPBACKNET << IN_CLASSA_NSHIFT))  ||
#else
        !((addr & 0xFF000000) ^ (INADDR_LOOPBACK-1))  || /* 127/8 */
#endif /*IN_LOOPBACK*/
        /* private [non-routable] IP ranges, according to RFC1918 */
        !((addr & 0xFF000000) ^ 0x0A000000)  || /* 10/8                      */
        !((addr & 0xFFF00000) ^ 0xAC100000)  || /* 172.16.0.0-172.31.255.255 */
        !((addr & 0xFFFF0000) ^ 0xC0A80000)  || /* 192.168/16                */
        /* multicast IP range is also excluded: 224.0.0.0-239.255.255.255 */
#if   defined(IN_MULTICAST)
        IN_MULTICAST(addr)
#elif defined(IN_CLASSD)
        IN_CLASSD(addr)
#else
        !((addr & 0xF0000000) ^ 0xE0000000)
#endif /*IN_MULTICAST*/
        ;
}


extern int/*bool*/ NcbiIsPrivateIP(unsigned int ip)
{
    return x_IsPrivateIP(SOCK_NetToHostLong(ip));
}


#ifdef __GNUC__
inline
#endif /*__GNUC__*/
static int/*bool*/ x_IsAPIPA(unsigned int addr)
{
    return !((addr & 0xFFFF0000) ^ 0xA9FE0000); /* 169.254/16 per IANA */
}


extern int/*bool*/ NcbiIsAPIPA(unsigned int ip)
{
    return x_IsAPIPA(SOCK_NetToHostLong(ip));
}


#ifdef __GNUC__
inline
#endif /*__GNUC__*/
static int/*bool*/ s_IsPrivateIP(unsigned int ip)
{
    unsigned int addr = SOCK_NetToHostLong(ip);
    return x_IsPrivateIP(addr)  ||  x_IsAPIPA(addr);
}


/* NB: "str" is either NULL or non-empty */
static const TNCBI_IPv6Addr* x_StringToAddr(TNCBI_IPv6Addr* addr,
                                            const char*     str)
{
    const char* end = NcbiStringToAddr(addr, str, 0);
    assert(!str  ||  *str);
    if (str  /*&&  *str*/  &&  (!end  ||  *end))
        NcbiIPv4ToIPv6(addr, SOCK_gethostbyname(str), 0);
    return addr;
}


/* NB: returns either NULL or non-empty value for the "name" */
static const char* s_SearchTrackingEnv(const char*        name,
                                       const char* const* tracking_env)
{
    const char* result;

    if (!tracking_env) {
        // CORE_LOCK_READ;
        result = getenv(name);
        // CORE_UNLOCK;
#ifdef NCBI_LOCALNET_DEBUG
        CORE_LOGF(eLOG_Trace, ("Getenv('%s') = %s%s%s", name,
                               result ? "\""   : "",
                               result ? result : "NULL",
                               result ? "\""   : ""));
#endif /*NCBI_LOCALNET_DEBUG*/
    } else {
        size_t len = strlen(name);
        const char* const* str;
        result = 0;
        for (str = tracking_env;  *str;  ++str) {
            if (strncasecmp(*str, name, len) == 0  &&  (*str)[len] == '=') {
                result = &(*str)[++len];
                break;
            }
        }
#ifdef NCBI_LOCALNET_DEBUG
        CORE_LOGF(eLOG_Trace, ("Tracking '%s' = %s%s%s", name,
                               result ? "\""   : "",
                               result ? result : "NULL",
                               result ? "\""   : ""));
#endif /*NCBI_LOCALNET_DEBUG*/
    }
    return result  &&  *(result += strspn(result, " \t")) ? result : 0;
}


static const char* s_SearchForwardedFor(TNCBI_IPv6Addr*    addr,
                                        const char* const* tracking_env)
{
    const char* f = s_SearchTrackingEnv("HTTP_X_FORWARDED_FOR", tracking_env);
    TNCBI_IPv6Addr xaddr;
    int/*bool*/ external;
    char *p, *q, *r, *s;

    if (!f)
        return 0;
    r = 0;
    external = !(s = strdup(f)) ? 1/*T*/ : 0/*F*/;
    for (p = s;  p  &&  *p;  p = q + strspn(q, ", \t")) {
        int/*bool*/ private_ip;
        q = p + strcspn(p, ", \t");
        if (*q)
            *q++ = '\0';
        if (!*p  ||  NcbiIsEmptyIPv6(x_StringToAddr(&xaddr, p))) {
#ifdef NCBI_LOCALNET_DEBUG
            CORE_LOG(eLOG_Trace, "Forcing external");
#endif /*NCBI_LOCALNET_DEBUG*/
            external = 1/*true*/;
            r        = 0/*error*/;
        } else if (!(private_ip = s_IsPrivateIP(NcbiIPv6ToIPv4(&xaddr, 0)))
                   &&  !NcbiIsLocalIPEx(&xaddr, 0)) {
            r        = p;
            *addr    = xaddr;
            break;
        } else if (!external
                   &&  (!r
                        ||  (!private_ip
                             &&  s_IsPrivateIP(NcbiIPv6ToIPv4(addr, 0))))) {
            r        = p;
            *addr    = xaddr;
        }
    }
    if (r) {
        memmove(s, r, strlen(r) + 1);
        assert(*s  &&  !NcbiIsEmptyIPv6(addr));
        return s;
    }
    if (s)
        free(s);
    memset(addr, 0, sizeof(*addr));
    return external ? "" : 0;
}


/* The environment checked here must be in correspondence with the tracking
 * environment created by CTrackingEnvHolder::CTrackingEnvHolder()
 * (header: <cgi/ncbicgi.hpp>, source: cgi/ncbicgi.cpp, library: xcgi)
 */
extern TNCBI_IPv6Addr NcbiGetCgiClientIPv6Ex(ECgiClientIP       flag,
                                             char*              buf,
                                             size_t             buf_size,
                                             const char* const* tracking_env)
{
    struct {
        const char*    host;
        TNCBI_IPv6Addr addr;
    } probe[4];
    int/*bool*/ external = 0/*false*/;
    const char* host = 0;
    TNCBI_IPv6Addr addr;
    size_t i;

    memset(&addr, 0, sizeof(addr));
    memset(probe, 0, sizeof(probe));
    for (i = 0;  i < SizeOf(probe);  ++i) {
        int/*bool*/ bad_addr = 1/*true*/;
        switch (i) {
        case 0:
            probe[i].host = s_SearchTrackingEnv("HTTP_CAF_PROXIED_HOST",
                                                tracking_env);
            break;
        case 1:
            /* NB: sets probe[i].addr */
            probe[i].host = s_SearchForwardedFor(&probe[i].addr,
                                                 tracking_env);
            bad_addr = 0/*false*/;
            break;
        case 2:
            probe[i].host = s_SearchTrackingEnv("PROXIED_IP",
                                                tracking_env);
            break;
        case 3:
            probe[i].host = s_SearchTrackingEnv("HTTP_X_FWD_IP_ADDR",
                                                tracking_env);
            break;
        default:
            assert(0);
            continue;
        }
        if (!probe[i].host)
            continue;
        assert(*probe[i].host  ||  !bad_addr/*forwarded_for*/);
        if ( bad_addr)
            x_StringToAddr(&probe[i].addr, probe[i].host);
        bad_addr = NcbiIsEmptyIPv6(&probe[i].addr);
        if (!bad_addr  &&  NcbiIsLocalIPEx(&probe[i].addr, 0))
            continue;
#ifdef NCBI_LOCALNET_DEBUG
        CORE_LOG(eLOG_Trace, "External on");
#endif /*NCBI_LOCALNET_DEBUG*/
        external = 1/*true*/;
        if (bad_addr/*unresolvable*/
            ||  (bad_addr = !s_IsPrivateIP(NcbiIPv6ToIPv4(&probe[i].addr, 0)))
            ||  !host/*1st occurrence*/) {
            assert(probe[i].host);
            host = probe[i].host;
            addr = probe[i].addr;
            if (bad_addr)
                break;
        }
    }
    if (!external) {
        assert(NcbiIsEmptyIPv6(&addr));
        for (i = flag == eCgiClientIP_TryLeast ? 1 : 0;
             i <= (flag == eCgiClientIP_TryAll ? 8 : 7);  ++i) {
            switch (i) {
            case 0:
                host = s_SearchTrackingEnv("HTTP_CLIENT_HOST",
                                           tracking_env);
                break;
            case 1:
            case 2:
            case 3:
            case 4:
                host = probe[i - 1].host;
                break;
            case 5:
                host = s_SearchTrackingEnv("HTTP_X_REAL_IP",
                                           tracking_env);
                break;
            case 6:
                host = s_SearchTrackingEnv("REMOTE_HOST",
                                           tracking_env);
                if (host) {
                    /* consider as a special case per semantics in RFC-3875 */
                    x_StringToAddr(&addr, s_SearchTrackingEnv("REMOTE_ADDR",
                                                              tracking_env));
                }
                break;
            case 7:
                host = s_SearchTrackingEnv("REMOTE_ADDR",
                                           tracking_env);
                break;
            case 8:
                host = s_SearchTrackingEnv("NI_CLIENT_IPADDR",
                                           tracking_env);
                break;
            default:
                assert(0);
                continue;
            }
            if (host) {
                assert(*host);
                if (1 <= i  &&  i <= 4)
                    addr = probe[i - 1].addr;
                else if (i != 6/*REMOTE_HOST*/  ||  NcbiIsEmptyIPv6(&addr))
                    x_StringToAddr(&addr, host);
                break;
            }
        }
    }
    if (buf  &&  buf_size) {
        if (host  &&  (i = strlen(host)) < buf_size)
            memcpy(buf, host, ++i);
        else
            *buf = '\0';
    }
    if (probe[1].host  &&  *probe[1].host)
        free((void*) probe[1].host /*forwarded_for*/);
    return addr;
}


extern TNCBI_IPv6Addr NcbiGetCgiClientIPv6(ECgiClientIP       flag,
                                           const char* const* tracking_env)
{
    return NcbiGetCgiClientIPv6Ex(flag, 0, 0, tracking_env);
}


extern unsigned int NcbiGetCgiClientIPEx(ECgiClientIP       flag,
                                         char*              buf,
                                         size_t             buf_size,
                                         const char* const* tracking_env)
{
    TNCBI_IPv6Addr
        addr = NcbiGetCgiClientIPv6Ex(flag, buf, buf_size, tracking_env);
    return NcbiIPv6ToIPv4(&addr, 0);
}


extern unsigned int NcbiGetCgiClientIP(ECgiClientIP       flag,
                                       const char* const* tracking_env)
{
    return NcbiGetCgiClientIPEx(flag, 0, 0, tracking_env);
}


extern int/*bool*/ NcbiIsLocalCgiClient(const char* const* tracking_env)
{
    TNCBI_IPv6Addr addr;
    if (s_SearchTrackingEnv("HTTP_CAF_EXTERNAL", tracking_env))
        return 0/*false*/;
    if (s_SearchTrackingEnv("HTTP_NCBI_EXTERNAL", tracking_env))
        return 0/*false*/;
    addr = NcbiGetCgiClientIPv6(eCgiClientIP_TryAll, tracking_env);
    return NcbiIsLocalIPEx(&addr, 0);
}
