/*****************************************************************************/
/*
                                 GeoLocate.c

A utility to geolocate an IP address from a (semi-)free service.

Four to choose from (defaults to the first, which is the best IMHO):

http://ip-api.com/
http://ipwhois.app/
http://www.geoplugin.net/
http://geolocation-db.com/

Uses a geolocation cache to minimise lookups.


IP-API.COM RATE LIMIT
---------------------
"This endpoint is limited to 45 requests per minute from an IP address.

If you go over the limit your requests will be throttled (HTTP 429) until your
rate limit window is reset. If you constantly go over the limit your IP address
will be banned for 1 hour."

GeoLocation will either be steady (e.g. 1 every second or two, e.g. HTTPDMON)
or bursty (multiple being resolved at once, e.g. INTRUspect, QdLogStats). 
Steady will be  unlikely to exceed the threshold and therefore not needing
explicit rate-limiting.  If the usage retries non-resolved geolocations
(e.g. INTRUspect) then a no-delay option can be used.

NoDelay use requires setting GeoLocateNoDelay(1) to indicate this.
It also can be set 0 (the default) to emphasize it is using a delay.
If the rate limit delays a result the call returns "(waiting...)".

By default each call should return a result and so some calls may be delayed to
meet rate limits.  Where a call does not require a response it returns
immediately with an empty string ("").  To disable the default rate limit
delay call GeoLocateDelayed(0) before processing.


STANDALONE
----------
The utility can be used standalone where the default behaviour is to create DCL
symbols containing the location data.  By default, symbols are scoped global. 
Using the "/local" qualifier they can be created as local symbols.  Best to
delete existing symbols to avoid "hangovers" from previous geolocations.  For
example:

$ delete /symbol /global /all
$ mcr cgi-bin:[000000]geolocate 81.39.111.122
$ show sym geolocate*
  GEOLOCATE_ADDR == "81.39.111.122"
  GEOLOCATE_CITY == "Madrid"
  GEOLOCATE_COUNTRY == "Spain"
  GEOLOCATE_ERROR == ""
  GEOLOCATE_HOST == "122.red-81-39-111.dynamicip.rima-tde.net"
  GEOLOCATE_LAT == "40.4163"
  GEOLOCATE_LONG == "-3.6934"
  GEOLOCATE_MILLISECS == "103"
  GEOLOCATE_REGION == "Madrid"
  GEOLOCATE_SERVICE == "ip-api.com"

This is the default behaviour if used passinf the lookup as a parameter.


CGI/CGIPLUS
-----------
If the lookup is NOT passed as a parameter the query string may optionally be
used to pass a host name or address.  If not it defaults to the remote host
name or address of the client.

Used as CGI the rate limit may quickly be exceeded on a busy site. As a CGIplus
script the cache is maintained meaning the rate-limit is less likely to be
quickly exceeded.  Also CGIplus is an order of magnitude more responsive.

Logical name GEOLOCATE_SERVICE provides same function as /SERVICE qualifier.


OBJECT CODE
-----------
This utility can be used standalone or as an object module utilised by another
utility.  HTTPDMON.C and QDLOGSTATS.C both have optional geolocation builds.


GEOLOCATE_UTF8
--------------
GeoLocate is intended for VT terminals.  It has UTF-8 characters munged into an
unlikely ASCII character.  Defining the above logical name to "1" results in
supression of this.  This is intended for web pages with a charset=utf-8
content-type.  *NOT* for VT terminals.

If defined as a digit then 0 disables the munge (i.e. enables UTF-8) and 1
enables UTF-8.  If defined as a non-digit character then that is used as the
UTF-8 substition character.  The default is '~' (tilde).

The /UTF8 qualifier performs the same function at the command-line.


BUILD DETAILS
-------------
See BUILD_GEOLOCATE.COM procedure.


VERSION LOG
-----------

20-OCT-2024  MGD  v2.0.0, CGI and CGIplus
26-FEB-2022  MGD  initial
*/
/*****************************************************************************/

#define SOFTWAREVN "2.0.0"
#define SOFTWARENM "GEOLOCATE"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

#ifndef GEOLOCATE_OBJECT
#define GEOLOCATE_OBJECT 0
#endif

#ifndef GEOLOCATE_OPENSSL
#define GEOLOCATE_OPENSSL 0
#endif

#ifndef GEOLOCATE_SYMBOL
#define GEOLOCATE_SYMBOL 1
#endif

/* ensure BSD 4.4 structures  */
#define _SOCKADDR_LEN
/* BUT MultiNet BG driver does not support BSD 4.4 AF_INET addresses */
#define NO_SOCKADDR_LEN_4

#include <ints.h>
#include <math.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include <descrip.h>
#include <lib$routines.h>
#include <libclidef.h>
#include <lnmdef.h>
#include <ssdef.h>
#include <starlet.h>

#include <socket.h>
#include <in.h>
#include <netdb.h>
#include <inet.h>

#if GEOLOCATE_OPENSSL
#include <openssl/crypto.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#endif /* GEOLOCATE_OPENSSL */

#include "geolocate.h"

const static char  Utility [] = SOFTWARENM,
                   SoftwareId [] = SOFTWAREID;

#if GEOLOCATE_OBJECT
#undef GEOLOCATE_SYMBOL
#define GEOLOCATE_SYMBOL 0
extern int Debug;
#else
int Debug = 0;
#include "cgilib.h"
#endif

#define TIME64_ONE_SEC ((int64)10000000)

static char *GeoServices [] = { "ip-api.com",
                                "ipwhois.app",
                                "www.geoplugin.net",
                                "geolocation-db.com",
                                NULL };

static int  GeoBurstCount,
            GeoBurstLimit = 10,
            GeoBurstPeriod = 10,
            GeoDelaySeconds,
            GeoIsDelayed = 1,
            GeoIsNoDelay,
            GeoSymbolScope = LIB$K_CLI_GLOBAL_SYM,
            GeoIsUtf8,
            IsCgiPlus;
static int64  GeoBurstTime64,
              GeoCurrentTime64,
              GeoTotalMilliSecs;
static char  *GeoServicePtr,
             *GeoSubsChar = "~";

static int GeoIsAddress (char*);
static void GeoLocateMungeUniEsc (char*);
static void GeoLocateMungeUtf8 (char*);
static void GeoResponseSymbols ();
static void GeoSetSymbol (char*, char*, int);
static char* GeoTrnLnm (char*, char*, int);

int  GeoLocateFailCount;
int  GeoLocateHitCount;
int  GeoLocateLookupCount;
int  GeoLocateMilliSecs;
int  GeoLocateAveMilliSecs;
int  GeoLocateMaxMilliSecs;
int  GeoLocateMinMilliSecs;
char  *GeoLocateService;

/*****************************************************************************/
/*
*/

#if !GEOLOCATE_OBJECT

void CgiLocate ();

int main (int argc, char *argv[])

{
   int  cnt, idx, loop, full, out;
   char  *aservice, *alookup, *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   CgiLibEnvironmentInit (argc, argv, 0);
   if (IsCgiPlus = CgiLibEnvironmentIsCgiPlus()) CgiLocate ();

   full = out = 0;
   loop = 1;
   alookup = aservice = NULL;
   for (cnt = 1; cnt < argc; cnt++)
   {
      if (cnt == 1)
      {
         alookup = argv[cnt];
         continue;
      }
      if (!strcasecmp (argv[cnt], "/stdout"))
         out = 1;
      else
      if (!strcasecmp (argv[cnt], "/debug"))
         Debug = 1;
      else
      if (!strcasecmp (argv[cnt], "/dbug"))
         Debug = 1;
      else
      if (!strcasecmp (argv[cnt], "/full"))
         full = 1;
      else
      if (!strcasecmp (argv[cnt], "/local"))
         GeoSymbolScope = LIB$K_CLI_LOCAL_SYM;
      else
      if (!strcasecmp (argv[cnt], "/utf8"))
          GeoLocateUtf8 (1);
      else
      if (!strcasecmp (argv[cnt], "/utf8=1"))
          GeoLocateUtf8 (1);
      else
      if (!strncasecmp (argv[cnt], "/utf8=", 6) && argv[cnt][6])
         GeoSubsChar = argv[cnt]+6;
      else
      if (!strncasecmp (argv[cnt], "/service=", 9))
         aservice = argv[cnt] + 9;
      else
      if (!strcasecmp (argv[cnt], "/version"))
         fprintf (stdout, "%s\n", GeoLocateSoftwareId());
      else
      if (isdigit(argv[cnt][0]))
         loop = atoi(argv[cnt]);
   }

   if (!alookup) CgiLocate ();

   if (GeoTrnLnm ("GEOLOCATE_DEBUG", NULL, 0) != NULL) Debug = 1;

   if ((cptr = GeoTrnLnm ("GEOLOCATE_UTF8", NULL, 0)) != NULL)
   {
      if (isdigit(*cptr) && *cptr != '0') GeoLocateUtf8 (1);
      if (!isdigit(*cptr)) GeoSubsChar = strdup(cptr); 
   }

   if (argc > 1 && !strcasecmp (argv[1], "/version"))
      fprintf (stdout, "%s\n", GeoLocateSoftwareId());

   for (cnt = 1; cnt <= loop; cnt++)
   {
      while (sptr = GeoLocate (aservice, alookup))
      {
         GeoSetSymbol ("GEOLOCATE_ADDR", "", 0);
         GeoSetSymbol ("GEOLOCATE_CITY", "", 0);
         GeoSetSymbol ("GEOLOCATE_COUNTRY", "", 0);
         GeoSetSymbol ("GEOLOCATE_ERROR", "", 0);
         GeoSetSymbol ("GEOLOCATE_HOST", "", 0);
         GeoSetSymbol ("GEOLOCATE_LAT", "", 0);
         GeoSetSymbol ("GEOLOCATE_LONG", "", 0);
         GeoSetSymbol ("GEOLOCATE_MILLISECS", NULL, GeoLocateMilliSecs);
         GeoSetSymbol ("GEOLOCATE_REGION", "", 0);
         GeoSetSymbol ("GEOLOCATE_SERVICE", GeoLocateService, 0);

         if (GeoIsAddress (alookup))
         {
            GeoSetSymbol ("GEOLOCATE_ADDR", alookup, 0);
            cptr = GeoLocateLookup (NULL, alookup);
            if (*cptr == '[')
               GeoSetSymbol ("GEOLOCATE_ERROR", cptr, 0);
            else
               GeoSetSymbol ("GEOLOCATE_HOST", cptr, 0);
         }
         else
         {
            GeoSetSymbol ("GEOLOCATE_HOST", alookup, 0);
            cptr = GeoLocateLookup (alookup, NULL);
            if (*cptr == '[')
               GeoSetSymbol ("GEOLOCATE_ERROR", cptr, 0);
            else
               GeoSetSymbol ("GEOLOCATE_ADDR", cptr, 0);
         }

         if (sptr && *sptr == '[')
         {
            GeoSetSymbol ("GEOLOCATE_ERROR", sptr, 0);
            if (out) fprintf (stdout, "*** %s ***\n", sptr);
         }
         else
         {
            if (out)
               fprintf (stdout, "|%s| %dms lookup:%d hit:%d\n",
                        sptr, GeoLocateMilliSecs, GeoLocateLookupCount,
                        GeoLocateHitCount);

            cptr = sptr;
            while (*cptr && *cptr != GEOSEP) cptr++;
            if (*cptr) cptr++;
            for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
            if (*cptr)
            {
               *cptr = '\0';
               GeoSetSymbol ("GEOLOCATE_COUNTRY", sptr, 0);
               *cptr++ = GEOSEP;
            }
            for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
            if (*cptr)
            {
               *cptr = '\0';
               GeoSetSymbol ("GEOLOCATE_REGION", sptr, 0);
               *cptr++ = GEOSEP;
            }
            for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
            if (*cptr)
            {
               *cptr = '\0';
               GeoSetSymbol ("GEOLOCATE_CITY", sptr, 0);
               *cptr++ = GEOSEP;
            }
            for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
            if (*cptr)
            {
               *cptr = '\0';
               GeoSetSymbol ("GEOLOCATE_LAT", sptr, 0);
               *cptr++ = GEOSEP;
            }
            for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
            if (*cptr)
            {
               *cptr = '\0';
               GeoSetSymbol ("GEOLOCATE_LONG", sptr, 0);
               *cptr++ = GEOSEP;
            }
         }

         if (full) GeoResponseSymbols ();

         if (!aservice || *aservice != '*') break;
         GeoLocateResponse (NULL, NULL, NULL);
      }
   }
}

#endif /* GEOLOCATE_OBJECT */

/*****************************************************************************/
/*
Executing as a CGI or CGIplus script.
*/

#if !GEOLOCATE_OBJECT

void CgiLocate ()

{
   char  *aservice, *aptr, *cptr, *eptr, *hptr, *qptr, *sptr;
   char  buf [256];

   /*********/
   /* begin */
   /*********/

   if ((cptr = GeoTrnLnm ("GEOLOCATE_UTF8", NULL, 0)) != NULL)
   {
      if (isdigit(*cptr) && *cptr != '0') GeoLocateUtf8 (1);
      if (!isdigit(*cptr)) GeoSubsChar = strdup(cptr); 
   }

   for (;;)
   {
      /* block waiting for the next request */
      if (IsCgiPlus) CgiLibVar ("");

      if (GeoTrnLnm ("GEOLOCATE_DEBUG", NULL, 0) != NULL)
      {
         Debug = 1;
         fprintf (stdout, "content-type: text/plain\r\n\r\n");
         fflush(stdout);
      }

      aservice = GeoTrnLnm ("GEOLOCATE_SERVICE", NULL, 0);

      setenv ("ADDR", "", 1);
      setenv ("CITY", "", 1);
      setenv ("COUNTRY", "", 1);
      setenv ("ERROR", "", 1);
      setenv ("HOST", "", 1);
      setenv ("LAT", "", 1);
      setenv ("LONG", "", 1);
      setenv ("REGION", "", 1);

      aptr = eptr = hptr = qptr = sptr = NULL;

      if (sptr = CgiLibVarNull ("PATH_TRANSLATED"))
      {
         /* is this call from SSI document? */
         for (cptr = sptr; *cptr; *cptr++);
         while (cptr > sptr && *cptr != '.') cptr--;
         sptr = NULL;
         /* if no then interrogate query string */
         if (strcasecmp (cptr, ".shtml"))
            if (sptr = qptr = CgiLibVarNull ("QUERY_STRING"))
               if (!*sptr) sptr = NULL;
      }

      if (!(aptr = CgiLibVarNull ("REMOTE_ADDR")))
         eptr = "[addr?]";
      if (!(hptr = CgiLibVarNull ("REMOTE_HOST")))
         eptr = "[host?]";

      if (Debug) fprintf (stdout, "|%s|%s|%s|%s|\n", sptr, aptr, hptr, eptr);

      if (!sptr) sptr = aptr;
      if (!sptr) sptr = hptr;

      if (sptr)
      {
         if (GeoIsAddress (sptr))
         {
            setenv ("ADDR", aptr = sptr, 1);
            if (sptr == qptr)
            {
               /* address is from the query string, look up the host name */
               sptr = GeoLocateLookup (NULL, sptr);
               if (*sptr == '[')
                  setenv ("HOST", qptr, 1);
               else
                  setenv ("HOST", sptr, 1);
            }
            else
               setenv ("HOST", hptr, 1);
         }
         else
         {
            setenv ("HOST", sptr, 1);
            if (sptr == qptr)
            {
               /* host name is from the query string, look up the address */
               sptr = GeoLocateLookup (sptr, NULL);
               setenv ("ADDR", aptr = sptr, 1);
            }
            else
               setenv ("ADDR", aptr, 1);
         }
      }

      if (Debug) fprintf (stdout, "|%s|%s|%s|\n", aptr, hptr, eptr);

      sptr = GeoLocate (aservice, aptr);

      CgiLibResponseHeader (200, "text/plain",
                            "Script-Control: X-content-encoding-gzip=0\n");
      if (eptr)
         fprintf (stdout, "%s\n", eptr);
      else
      if (*sptr != '(')
      {
         cptr = sptr;
         while (*cptr && *cptr != GEOSEP) cptr++;
         if (*cptr) cptr++;
         for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
         if (*cptr)
         {
            *cptr = '\0';
            setenv ("COUNTRY", sptr, 1);
            *cptr++ = GEOSEP;
         }
         for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
         if (*cptr)
         {
            *cptr = '\0';
            setenv ("REGION", sptr, 1);
            *cptr++ = GEOSEP;
         }
         for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
         if (*cptr)
         {
            *cptr = '\0';
            setenv ("CITY", sptr, 1);
            *cptr++ = GEOSEP;
         }
         for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
         if (*cptr)
         {
            *cptr = '\0';
            setenv ("LAT", sptr, 1);
            *cptr++ = GEOSEP;
         }
         for (sptr = cptr; *cptr && *cptr != GEOSEP; cptr++);
         if (*cptr)
         {
            *cptr = '\0';
            setenv ("LONG", sptr, 1);
            *cptr++ = GEOSEP;
         }

         sptr = buf;
         if (!strcmp (getenv("HOST"), getenv("ADDR")))
            sptr += sprintf (sptr, "%s", getenv("ADDR"));
         else
            sptr += sprintf (sptr, "%s (%s)", getenv("HOST"), getenv("ADDR"));
         if (*(cptr = getenv("COUNTRY")))
            sptr += sprintf (sptr, " %s", cptr);
         if (*(cptr = getenv("REGION")))
            sptr += sprintf (sptr, " / %s", cptr);
         if (*(cptr = getenv("CITY")))
            sptr += sprintf (sptr, " / %s", cptr);

         fprintf (stdout, "%s\n", buf);
      }
      else
         fprintf (stdout, "%s\n", sptr);

      if (!IsCgiPlus) break;

      Debug = 0;

      CgiLibCgiPlusEOF ();
   }

   exit (0);
}

#endif /* GEOLOCATE_OBJECT */

/*****************************************************************************/
/*
Return geolocation data for |alookup| using |aservice| service.
Errors are returned with a leading null character.
*/

char* GeoLocate (char* aservice, char* alookup)

{
   static int  ServiceIndex;
   static char  LogValue [256],
                LookupError [64];
   static char  *agent = "WASD geolocate";

   int  idx;
   uint  second;
   int64  ts64,
          tf64;
   short  port;
   char  *aptr, *sptr, *service, *lookup;
   char  request [256],
         string [256];
   struct in_addr  ipaddr;
   struct hostent  *heptr;

   /*********/
   /* begin */
   /*********/

   if (aptr = GeoLocateResponse (NULL, alookup, NULL))
   {
      /********************/
      /* hit cached entry */
      /********************/

      GeoLocateMilliSecs = 0;
      return (aptr);
   }

   /***********/
   /* service */
   /***********/

   sys$gettim (&ts64);

   lookup = alookup;
   service = aservice;
   if (service && service[0] == '*')
   {
      if (service = GeoServices[ServiceIndex++])
         service++;
      else
         return (NULL);
   }
   while (!service)
   {
      if (LogValue[0] == '*')
      {
         if (service = GeoServices[ServiceIndex++])
            service++;
         else
            ServiceIndex = 0;
      }
      else
      if (!LogValue[0])
         service = GeoServices[0];

      if (!service)
         GeoTrnLnm (GeoLocateLogicalName(NULL), LogValue, ServiceIndex);
   }

   /******************/
   /* verify service */
   /******************/

   for (idx = 0; GeoServices[idx]; idx++)
   {
      for (sptr = GeoServices[idx]; !isalpha(*sptr); sptr++);
      if (!strcasecmp (service, sptr)) break;
   }
   if (!GeoServices[idx])
   {
      GeoLocateFailCount++;
      return ("[GeoService?]");
   }
   GeoLocateService = sptr;
   GeoServicePtr = GeoServices[idx];

   if (!GeoIsAddress (lookup))
   {
      /******************/
      /* not IP address */
      /******************/

      aptr = GeoLocateLookup (lookup, NULL);
      if (*aptr == '[')
      {
         GeoLocateFailCount++;
         return (aptr);
      }
      lookup = aptr;
   }

   if (!strncmp (lookup, "127.0.0.1", 9) ||
       !strncmp (lookup, "192.168.", 8) ||
       !strncmp (lookup, "172.16.", 7) ||
       !strncmp (lookup, "10.", 3)) return ("[private range]");

   /*****************/
   /* rate-limited? */
   /*****************/

   if (GeoIsNoDelay)
   {
      /* burst maximum GeoBurstLimit requests each GeoBurstPeriod seconds */
      sys$gettim (&GeoCurrentTime64);

      if (GeoCurrentTime64 > GeoBurstTime64)
      {
         /* reset the limit every this many seconds */
         GeoBurstTime64 = GeoCurrentTime64 + (TIME64_ONE_SEC * GeoBurstPeriod);
         GeoBurstCount = 0;
      }

      /* if that number of geolocations exceeded then */
      if (GeoBurstCount++ > GeoBurstLimit)
         return ("(waiting...)");
   }
   else
   if (GeoBurstTime64)
   {
      /* from ip-api.com rate limiting */
      if (GeoCurrentTime64 > GeoBurstTime64)
         GeoBurstTime64 = 0;
      else
         return ("(waiting...)");
   }

   if (GeoIsDelayed)
   {
      /* if that number of geolocations exceeded then */
      if (GeoBurstCount++ > GeoBurstLimit)
      {
         sleep (GeoBurstPeriod);
         GeoBurstCount = 0;
      }
   }

   /****************/
   /* HTTP request */
   /****************/

   sptr = request;
   if (!strcasecmp (service, "ip-api.com"))
      sptr += sprintf (sptr, "GET /json/%s", lookup);
   else
   if (!strcasecmp (service, "ipwhois.app"))
      sptr += sprintf (sptr, "GET /json/%s", lookup);
   else
   if (!strcasecmp (service, "www.geoplugin.net"))
      sptr += sprintf (sptr, "GET /json.gp?ip=%s", lookup);
   else
   if (!strcasecmp (service, "geolocation-db.com"))
      sptr += sprintf (sptr, "GET /jsonp/%s", lookup);
   else
      exit (SS$_BUGCHECK);

   sprintf (sptr,
" HTTP/1.0\r\n\
Host: %s\r\n\
User-Agent: %s\n\
Connection: close\r\n\
\r\n",
               service, agent);

   if (Debug) fprintf (stdout, "|%s|\n|%s|\n", service, request);

   GeoLocateLookupCount++;
   aptr = GeoLocateRequest (GeoServicePtr, alookup, request);
   if (!aptr[0] && aptr[1]) GeoLocateFailCount++;

   /***************/
   /* timekeeping */
   /***************/

   sys$gettim (&tf64);
   GeoLocateMilliSecs = (int)((tf64 - ts64) / (int64)10000);

   if (GeoLocateMilliSecs)
   {
      GeoTotalMilliSecs += GeoLocateMilliSecs;

      if (!GeoLocateMinMilliSecs)
          GeoLocateMinMilliSecs = GeoLocateMilliSecs;
      else
      if (GeoLocateMilliSecs < GeoLocateMinMilliSecs)
          GeoLocateMinMilliSecs = GeoLocateMilliSecs;

      if (GeoLocateMilliSecs > GeoLocateMaxMilliSecs)
         GeoLocateMaxMilliSecs = GeoLocateMilliSecs;

      if (GeoLocateLookupCount)
         GeoLocateAveMilliSecs = (int)(GeoTotalMilliSecs /
                                       GeoLocateLookupCount);
   }

   return (aptr);
}

/*****************************************************************************/
/*
Connect to the |service| geolocation server and send an HTTP request.
*/

char* GeoLocateRequest (char* service, char* lookup, char* request)

{
   static char  response [2048];

   int  cnt, err, secure, sock;
   char  *sptr;
   struct hostent  *heptr;
   struct sockaddr_in  serv_addr;
   struct in_addr  ipaddr;

#if GEOLOCATE_OPENSSL

   SSL_CTX  *ctx;
   SSL  *ssl;
   const SSL_METHOD  *meth;
   X509  *server_cert;
   EVP_PKEY  *pkey;

#endif /* GEOLOCATE_OPENSSL */

   /*********/
   /* begin */
   /*********/

   if (secure = (*service == '+')) service++;

   ipaddr.s_addr = inet_addr (service);
   if (ipaddr.s_addr == INADDR_NONE)
   {
      heptr = gethostbyname(service);
      if (heptr == NULL) return ("[gethostbyname()]");
   }

   sock = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP);       
   if (!sock) return ("[socket()]");

   memset (&serv_addr, 0, sizeof(serv_addr));
   serv_addr.sin_family = AF_INET;

   if (secure)
#if GEOLOCATE_OPENSSL
      serv_addr.sin_port = htons(443);
#else /* GEOLOCATE_OPENSSL */
      return ("[OpenSSL not compiled option]");
#endif /* GEOLOCATE_OPENSSL */
   else
      serv_addr.sin_port = htons(80);

   if (ipaddr.s_addr != INADDR_NONE)
      memcpy (&serv_addr.sin_addr.s_addr, &ipaddr.s_addr,
                                          sizeof(ipaddr.s_addr));
   else
      memcpy (&serv_addr.sin_addr.s_addr, heptr->h_addr, heptr->h_length);

   err = connect (sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)); 
   if (err) return ("[connect()]");

#if GEOLOCATE_OPENSSL

   if (secure)
   {
      /***********/
      /* TLS/SSL */
      /***********/

      SSL_library_init();
      SSL_load_error_strings();

      meth = TLS_client_method();

      ctx = SSL_CTX_new(meth);                        
      if (!ctx) return ("[SSL_CTX_new()]");

      ssl = SSL_new (ctx);
      if (!ssl) return ("[SSL_new()]");

      SSL_set_fd (ssl, sock);

      err = SSL_connect(ssl);
      if (err) return ("[SSL_connect()]");

      server_cert = SSL_get_peer_certificate (ssl);       
	
      if (Debug)
      {
         printf ("SSL connection using %s\n", SSL_get_cipher (ssl));

         if (server_cert != NULL)
         {
            fprintf (stdout, "server certificate:\n");

            sptr = X509_NAME_oneline(X509_get_subject_name(server_cert),0,0);
            if (!sptr) return ("[X509_NAME_oneline()]"); 
            fprintf (stdout, "\t subject: %s\n", sptr);
            free (sptr);

            sptr = X509_NAME_oneline(X509_get_issuer_name(server_cert),0,0);
            if (!sptr) return ("[X509_NAME_oneline()]"); 
            fprintf (stdout, "\t issuer: %s\n", sptr);
            free(sptr);

            X509_free (server_cert);
         }
         else
            fprintf(stdout, "The SSL server does not have certificate.\n");
      }

      err = SSL_write (ssl, request, strlen(request));  
      if (err) return ("[SSL_write()]");

      response[0] = '\0';
      err = SSL_read (ssl, response, sizeof(response)-1);                     
      if (err) return ("[SSL_read() response]");
      response[err] = '\0';
      if (Debug) fprintf (stdout, "received %d chars:'%s'\n", err, response);

      SSL_shutdown(ssl);
      err = SSL_shutdown(ssl);
      if (err) return ("[SSL_shutdown()]");

      SSL_free(ssl);

      err = close(sock);
      if (err) return ("[close()]");

      SSL_CTX_free(ctx);
   }
   else
#endif /* GEOLOCATE_OPENSSL */
   {
      /*************/
      /* cleartext */
      /*************/

      err = write (sock, request, strlen(request));  
      if (!err) return ("[write()]");

      response[0] = '\0';
      err = read (sock, response, sizeof(response)-1);                     
      if (!err) return ("[read() response]");
      response[err] = '\0';
      if (Debug) fprintf (stdout, "received %d\n|%s|\n", err, response);

      err = close(sock);
      if (err) return ("[close()]");
   }

   sptr = GeoLocateResponse (service, lookup, response);

   return (sptr);
}

/*****************************************************************************/
/*
If |service| is NULL then check the cache for a |lookup| entry.  Return if
found.

If |service| is non-NULL then parse the response according to the |service|
scheme.  Enter into the cache and return.

If |response| is NULL then return a pointer to the full response for parsing.
*/

char* GeoLocateResponse (char* service, char* lookup, char* response)

{
   static int  CacheCount = 0,
               CacheMax = 0,
               NextCache = 0,
               PrevIdx = 0;
   static int  HitIdx = -1;
   static char  huh = '~';
   static char  msg [16];
   /* lookup entry + ident entry + raw entry */
   struct  CacheEntry { char *lent; char *dent; char *rent; };
   static struct CacheEntry  *CacheData = NULL;

   int  ch, chr, count, dlen, idx, llen, rlen, status, utf8;
   int IpApiXrl = 0,
       IpApiXttl = 0,
       WhoIsAddrCount = 0;
   int64  tf64, ts64;
   char  *aptr, *cptr, *sptr, *zptr;
   char  buf [256];

   /*********/
   /* begin */
   /*********/

   if (!service && !lookup && !response)
   {
      /*****************/
      /* cache insight */
      /*****************/

      /* calculate the report space */
      count = 0;
      for (idx = 0; idx < CacheCount; idx++)
      {
         cptr = CacheData[idx].rent;
         while (*cptr) cptr++;
         count += cptr - CacheData[idx].lent + 32;
      }
      aptr = sptr = calloc (1, count + 64);
      /* populate the report as JSON entries */
      sptr += sprintf (sptr,
"{\"$data\":\"geolocate\",\"CacheCount\":%d,\"records\":[",
                       CacheCount);
      for (idx = 0; idx < CacheCount; idx++)
      {
         sptr += sprintf (sptr, "{\"idx%d\":\"%s$%s$",
                          idx, CacheData[idx].lent,
                               CacheData[idx].dent);
         for (cptr = CacheData[idx].rent; *cptr; cptr++)
            if (*cptr == '\"')
               *sptr++ = '\'';
            else
               *sptr++ = *cptr;
         for (cptr = "\"},"; *cptr; *sptr++ = *cptr++);
      }
      if (*(sptr-1) == ',') sptr--;  /* trailing comma */
      sptr += sprintf (sptr, "]}");
      return (aptr);
   }

   if (!lookup)
   {
      /*******************/
      /* reset the cache */
      /*******************/

      for (idx = 0; idx < CacheMax; idx++)
      {
         if (CacheData[idx].lent) free (CacheData[idx].lent);
         CacheData[idx].lent = CacheData[idx].dent = CacheData[idx].rent = NULL;
      }
      return (NULL);
   }

   if (!service)
   {
      /*****************/
      /* look in cache */
      /*****************/

      if (!CacheData) return (NULL);
      for (idx = PrevIdx; idx < CacheCount; idx++)
      {
         cptr = lookup;
         sptr = CacheData[idx].lent;
         while (*cptr && *sptr && tolower(*cptr) == tolower(*sptr))
         {
            cptr++;
            sptr++;
         }
         if (!*cptr && !*sptr)
         {
            /*******/
            /* hit */
            /*******/

            HitIdx = PrevIdx = idx;
            GeoLocateHitCount++;
            return (CacheData[idx].dent);
         }
         if (PrevIdx) idx = PrevIdx = 0;
      }

      /********/
      /* miss */
      /********/

      HitIdx = -1;
      idx = PrevIdx = 0;
      return (NULL);
   }

   if (!response)
   {
      /****************/
      /* response hit */
      /****************/

      if (HitIdx < 0) return (NULL);
      return (CacheData[HitIdx].rent);
   }

   /***************/
   /* HTTP header */
   /***************/

   for (aptr = response; *aptr && *aptr != ' '; aptr++);
   if (*aptr == ' ')
      status = atoi(aptr+1);
   else
      status = 0;
   if (status != 200)
   {
      sprintf (msg, "[HTTP:%d]", status);
      return (msg);
   }

   for (aptr = response;
        *aptr && *(ushort*)aptr != '\n\n' && *(ulong*)aptr != '\r\n\r\n';
        aptr++);
   if (*(ushort*)aptr == '\n\n')
   {
      *aptr = '\0';
      aptr += 2;
   }
   else
   if (*(ulong*)aptr == '\r\n\r\n')
   {
      *aptr = '\0';
      aptr += 4;
   }
   else
      return (NULL);

   utf8 = 0;
   if (strstr (response, "charset=utf-8")) utf8 = 1;
   if (!strcasecmp (service, "ip-api.com"))
   {
      if (cptr = strstr (response, "X-Rl: ")) IpApiXrl = atoi(cptr+6);
      if (cptr = strstr (response, "X-Ttl: ")) IpApiXttl = atoi(cptr+7);
   }

   response = aptr;
   if (Debug) fprintf (stdout, "UTF-8 %d\n|%s|\n", utf8, response);

   /***********************/
   /* cache HTTP response */
   /***********************/

   if (!CacheMax)
   {
      /* allocate an array of pointers to cache entries */
      CacheData = calloc (CacheMax = GeoLocateCacheMax(0),
                          sizeof(struct CacheEntry));
      if (!CacheData) exit (vaxc$errno);
   }

   if ((idx = NextCache++) >= CacheMax)
   {
      /* round-robin allocation */
      idx = NextCache = 0;
   }

   /* leave space for multiple GEOSEP and cache stats */
   zptr = (sptr = buf) + sizeof(buf)-32;
   for (cptr = lookup; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';
   llen = sptr - buf;

   if (utf8)
      if (!GeoIsUtf8)
         GeoLocateMungeUtf8 (response);
   GeoLocateMungeUniEsc (response);

   if (!strcasecmp (service, "ip-api.com"))
   {
      /**************/
      /* ip-api.com */
      /**************/

      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"status\":\""))
      {
         if (!strncmp (aptr+10, "fail", 4))
         {
            if (aptr = strstr (response, "\"message\":\""))
            {
               for (cptr = (aptr += 11);
                    *cptr && *cptr != '\"' && sptr < zptr;
                    *sptr++ = *cptr++);
            }
         }
      }
      if (aptr = strstr (response, "\"country\":\""))
      {
         for (cptr = (aptr += 11);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"regionName\":\""))
      {
         for (cptr = (aptr += 14);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"city\":\""))
      {
         for (cptr = (aptr += 8);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"lat\":"))
      {
         for (cptr = (aptr += 6);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"lon\":"))
      {
         for (cptr = (aptr += 6);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
   }
   else
   if (!strcasecmp (service, "ipwhois.app"))
   {
      /***************/
      /* ipwhois.app */
      /***************/

      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"success\":"))
      {
         if (!strncmp (aptr+10, "false", 4))
         {
            if (aptr = strstr (response, "\"message\":\""))
            {
               for (cptr = (aptr += 11);
                    *cptr && *cptr != '\"' && sptr < zptr;
                    *sptr++ = *cptr++);
            }
         }
      }
      if (aptr = strstr (response, "\"country\":\""))
      {
         for (cptr = (aptr += 11);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"region\":\""))
      {
         for (cptr = (aptr += 10);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"city\":\""))
      {
         for (cptr = (aptr += 8);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"latitude\":"))
      {
         for (cptr = (aptr += 11);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"longitude\":"))
      {
         for (cptr = (aptr += 12);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      if (aptr = strstr (response, "\"completed_requests\":"))
         WhoIsAddrCount = atoi(aptr + 21);
   }
   else
   if (!strcasecmp (service, "www.geoplugin.net"))
   {
      /*********************/
      /* www.geoplugin.net */
      /*********************/

      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"geoplugin_status\":"))
      {
         if (strncmp (aptr+19, "200", 3))
         {
            for (cptr = "status:"; *cptr; *sptr++ = *cptr++);
            *sptr++ = aptr[19];
            *sptr++ = aptr[20];
            *sptr++ = aptr[21];
         }
      }
      if (aptr = strstr (response, "\"geoplugin_countryName\":\""))
      {
         for (cptr = (aptr += 25);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"geoplugin_regionName\":\""))
      {
         for (cptr = (aptr += 24);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"geoplugin_city\":\""))
      {
         for (cptr = (aptr += 18);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"geoplugin_latitude\":\""))
      {
         for (cptr = (aptr += 22);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"geoplugin_longitude\":\""))
      {
         for (cptr = (aptr += 23);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      if (aptr = strstr (response, "\"geoplugin_longitude\":\""))
      {
         for (cptr = (aptr += 23);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
   }
   else
   if (!strcasecmp (service, "geolocation-db.com"))
   {
      /**********************/
      /* geolocation-db.com */
      /**********************/

      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"country_name\":\""))
      {
         for (cptr = (aptr += 16);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"state\":\""))
      {
         for (cptr = (aptr += 9);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"city\":\""))
      {
         for (cptr = (aptr += 8);
              *cptr && *cptr != '\"' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"latitude\":"))
      {
         for (cptr = (aptr += 11);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
      *sptr++ = GEOSEP;
      if (aptr = strstr (response, "\"longitude\":"))
      {
         for (cptr = (aptr += 12);
              *cptr && *cptr != ',' && sptr < zptr;
              *sptr++ = *cptr++);
      }
   }
   else
   {
      /* empty response */
      *sptr++ = GEOSEP;
      *sptr++ = GEOSEP;
      *sptr++ = GEOSEP;
      *sptr++ = GEOSEP;
      *sptr++ = GEOSEP;
   }
   *sptr = '\0';

   /******************/
   /* additonal data */
   /******************/

   if (WhoIsAddrCount)
      sptr += sprintf (sptr, "%c%d/%d/%d+",
                       GEOSEP, idx, NextCache, WhoIsAddrCount);
   else
      sptr += sprintf (sptr, "%c%d/%d+", GEOSEP, idx, NextCache);

   if (IpApiXrl <= 12 && IpApiXttl)
   {
      /****************************/
      /* ip-api.com rate limiting */
      /****************************/

      /* do not make another request until 2 x period */
      if (GeoIsNoDelay)
      {
         sys$gettim (&GeoCurrentTime64);
         GeoBurstTime64 = GeoCurrentTime64 +
                          (TIME64_ONE_SEC * GeoBurstPeriod * 2);
      }
      else
      {
         /* rate limit must be enforced */
         sleep (GeoBurstPeriod * 2);
      }
   }

   /**************/
   /* into cache */
   /**************/

   /* quick sanity check */
   if (sptr - buf >= sizeof(buf)-1) exit (SS$_BUGCHECK);
   dlen = sptr - buf;

   /* free any previous cache entry */
   if (CacheData[idx].lent)
   {
      free (CacheData[idx].lent);
      CacheData[idx].lent = CacheData[idx].dent = CacheData[idx].rent = NULL;
   }
   else
      CacheCount++;

   for (cptr = response; *cptr; *cptr++);
   rlen = cptr - response;
   CacheData[idx].lent = calloc (1, llen + dlen + rlen + 3);
   if (!CacheData[idx].lent) exit (vaxc$errno); 

   /* three consecutive null-terminated strings */
   sptr = CacheData[idx].lent;
   for (cptr = lookup; *cptr; *sptr++ = *cptr++);
   *sptr++ = '\0';
   CacheData[idx].dent = sptr;
   for (cptr = buf; *cptr; *sptr++ = *cptr++);
   *sptr++ = '\0';
   CacheData[idx].rent = sptr;
   for (cptr = response; *cptr; *sptr++ = *cptr++);
   *sptr = '\0';

   if (Debug) fprintf (stdout, "%d\n|%s|\n|%s|\n|%s|\n",
                       llen + dlen + rlen + 3,
                       CacheData[idx].lent,
                       CacheData[idx].dent,
                       CacheData[idx].rent);

   return (CacheData[HitIdx = idx].dent);
}

/****************************************************************************/
/*
*/

char* GeoLocateData (char* geoptr, int extend)

{
   static char  buf [128+32];

   char  *cptr, *sptr, *zptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoLocateData()\n");

   if (!(cptr = geoptr) || !*cptr) return ("[bugcheck]");

   zptr = (sptr = buf) + sizeof(buf)-32;
   if (cptr && *cptr == '[')
   {
      /* error message */
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
   }
   else
   if (cptr)
   {
      /* span over IP address */
      while (*cptr && *cptr != GEOSEP) cptr++;
      if (*cptr) cptr++;
   
      /* country */
      while (*cptr && *cptr != GEOSEP) *sptr++ = *cptr++;
      if (*cptr) cptr++;
   
      /* region */
      if (*cptr != GEOSEP)
      {
         *sptr++ = ' ';
         *sptr++ = '/';
         *sptr++ = ' ';
      }
      while (*cptr && *cptr != GEOSEP && sptr < zptr) *sptr++ = *cptr++;
      if (*cptr) cptr++;
   
      /* city */
      if (*cptr != GEOSEP)
      {
         *sptr++ = ' ';
         *sptr++ = '/';
         *sptr++ = ' ';
      }
      while (*cptr && *cptr != GEOSEP && sptr < zptr) *sptr++ = *cptr++;
   
      if (extend)
      {
         if (*cptr) cptr++;
         /* latitude */
         if (*cptr != GEOSEP)
         {
            *sptr++ = ' ';
            *sptr++ = '/';
            *sptr++ = ' ';
         }
         while (*cptr && *cptr != GEOSEP && sptr < zptr) *sptr++ = *cptr++;
         if (*cptr) cptr++;
   
         /* longitude */
         if (*cptr != GEOSEP)
         {
            *sptr++ = ',';
            *sptr++ = ' ';
         }
         while (*cptr && *cptr != GEOSEP && sptr < zptr) *sptr++ = *cptr++;
         if (*cptr) cptr++;
   
         /* cache stats */
         *sptr++ = ',';
         *sptr++ = ' ';
         while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      }
   }
   *sptr = '\0';

   return (buf);
}

/****************************************************************************/
/*
Parse the full JSON response one field at a time returning a pointer to a
buffer containing "<name>\0<value>\0" and NULL when fully parsed.
*/

char* GeoLocateParse (char **response)

{
   static char  *aptr = NULL;

   int  len;
   char  *cptr, *sptr, *zptr;
   char  buf [512];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoResponseParse()\n");

   if (aptr) free (aptr);

   if (!*response) return (NULL);

   cptr = *response;
   zptr = (sptr = buf) + sizeof(buf)-1;
   while (*cptr && *cptr != '\"') cptr++;
   if (*cptr) cptr++;
   while (*cptr && *cptr != '\"' && sptr < zptr)
   {
      if (*cptr == '\\' && *(cptr+1)) cptr++;
      *sptr++ = *cptr++;
   }
   *sptr++ = '\0';
   len = sptr - buf;
   if (Debug) fprintf (stdout, "|%s|\n", buf);
   while (*cptr && *cptr != '\"') cptr++;
   if (*cptr == '\"') cptr++;

   while (*cptr && *cptr != ':') cptr++;
   if (*cptr) cptr++;
   while (*cptr && isspace(*cptr)) cptr++;

   if (*cptr == '\"')
   {
      cptr++;
      while (*cptr && *cptr != '\"' && sptr < zptr)
      {
         if (*cptr == '\\' && *(cptr+1)) cptr++;
         *sptr++ = *cptr++;
      }
      while (*cptr && *cptr != '\"') cptr++;
      if (*cptr == '\"') cptr++;
   }
   else
   {
      while (*cptr && *cptr != ',' && !isspace(*cptr) && sptr < zptr)
         *sptr++ = *cptr++;
      while (*cptr && *cptr != ',' && !isspace(*cptr)) cptr++;
   }
   *sptr++ = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", buf + len);
   len = sptr - buf;

   while (*cptr && isspace(*cptr)) cptr++;
   if (*cptr == ',') cptr++;
   while (*cptr && isspace(*cptr)) cptr++;

   if (*cptr == '}')
      *response = NULL;
   else
      *response = cptr;

   aptr = calloc (1, len);
   if (!aptr) exit (vaxc$errno);
   zptr = (sptr = aptr) + len;
   for (cptr = buf; sptr < zptr; *sptr++ = *cptr++);
   return (aptr);
}

/*****************************************************************************/
/*
If |name| is non-NULL lookup the IP address using the host name.
If |addr| is non-NULL lookup the host name using the address.
*/

char* GeoLocateLookup
(
char *name,
char *addr
)
{
   static char  buf [256];

   int  retry, retval;
   char  *cptr, *sptr, *zptr;
   void  *addptr;
   struct sockaddr_in  addr4;
   struct sockaddr_in6  addr6;
   struct addrinfo  hints;
   struct addrinfo  *aiptr, *resaiptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoLocateLookup() |%s|%s|\n", name, addr);

   if (addr)
   {
      retval = 0;
      memset (&addr4, 0, sizeof(addr4));
      if (inet_pton (AF_INET, addr, &addr4.sin_addr) > 0)
      {
         /* MultiNet does not support BSD 4.4 AF_INET addresses */
#ifdef NO_SOCKADDR_LEN_4
         /* this kludge seems to work for both! */
         *(__unaligned short*)&addr4 = AF_INET;
#else
         addr4.sin_len = sizeof(struct sockaddr_in);
         addr4.sin_family = AF_INET;
#endif
         for (retry = LOOKUP_RETRY; retry; retry--)
         {
            retval = getnameinfo ((struct sockaddr*)&addr4, sizeof(addr4),
                                  buf, sizeof(buf),
                                  NULL, 0, NI_NAMEREQD);
            if (retval != EINTR && retval != EAI_AGAIN) break;
            sleep (1);
         }
         if (retval)
         {
            if (Debug) fprintf (stdout, "a) %d\n", retval);
            if (retval == EAI_NONAME)
               strcpy (buf, "[unresolved]");
            else
            if (retval == EAI_FAIL || retval == EAI_AGAIN)
               strcpy (buf, "[failed]");
            else
            if (retval == EAI_SYSTEM)
               sprintf (buf, "[%s %d]", strerror(errno), errno);
            else
               sprintf (buf, "[%s %d]", gai_strerror(retval), retval);
         }
         return (buf);
      }
      else
      {
         memset (&addr6, 0, sizeof(addr6));
         if (inet_pton (AF_INET6, addr, &addr6.sin6_addr) > 0)
         {
            addr6.sin6_len = sizeof(struct sockaddr_in6);
            addr6.sin6_family = AF_INET6;
            for (retry = LOOKUP_RETRY; retry; retry--)
            {
               retval = getnameinfo ((struct sockaddr*)&addr6, addr6.sin6_len,
                                     buf, sizeof(buf),
                                     NULL, 0, NI_NAMEREQD);
               if (retval != EINTR && retval != EAI_AGAIN) break;
               sleep (1);
            }
            if (retval)
            {
               if (Debug) fprintf (stdout, "b) %d\n", retval);
               if (retval == EAI_NONAME)
                  strcpy (buf, "[unresolved]");
               else
               if (retval == EAI_FAIL || retval == EAI_AGAIN)
                  strcpy (buf, "[failed]");
               else
               if (retval == EAI_SYSTEM)
                  sprintf (buf, "[%s %d]", strerror(errno), errno);
               else
                  sprintf (buf, "[%s %d]", gai_strerror(retval), retval);
            }
         }
         return (buf);
      }
   }

   if (name)
   {
      aiptr = NULL;
      memset (&hints, 0, sizeof(hints));
      hints.ai_flags |= AI_CANONNAME;
      retval = 0;
      for (retry = LOOKUP_RETRY; retry; retry--)
      {
         retval = getaddrinfo (name, NULL, &hints, &resaiptr);
         if (retval != EINTR && retval != EAI_AGAIN) break;
         sleep (1);
      }
      if (retval)
      {
         if (Debug) fprintf (stdout, "c) %d\n", retval);
         if (retval == EAI_NONAME)
            strcpy (buf, "[unresolved]");
         else
         if (retval == EAI_FAIL || retval == EAI_AGAIN)
            strcpy (buf, "[failed]");
         else
         if (retval == EAI_SYSTEM)
            sprintf (buf, "[%s %d]", strerror(errno), errno);
         else
            sprintf (buf, "[%s %d]", gai_strerror(retval), retval);
         return (buf);
      }
      else
      {
         /* potentially multiple addresses for the one host name */
         zptr = (sptr = buf) + sizeof(buf)-8;

         /* first IPv4 */
         for (aiptr = resaiptr; aiptr; aiptr = aiptr->ai_next)
         {
            addptr = &((struct sockaddr_in *)aiptr->ai_addr)->sin_addr;
            if (aiptr->ai_family == AF_INET) break;
         }
         if (!aiptr)
         {
            /* then IPv6 */
            for (aiptr = resaiptr; aiptr; aiptr = aiptr->ai_next)
            {
               addptr = &((struct sockaddr_in6 *)aiptr->ai_addr)->sin6_addr;
               if (aiptr->ai_family != AF_INET6) break;
            }
         }
         if (!aiptr)
         {
            strcpy (buf, "[not AF_INET or AF_INET6]");
            return (buf);
         }

         if (sptr > buf) *sptr++ = ' ';
         if (!inet_ntop (aiptr->ai_family, addptr, sptr, zptr - sptr))
            sprintf (buf, "[%s %d]", strerror(errno), errno);

         return (buf);
      }

      /* free the addrinfo */
      freeaddrinfo(aiptr);
   }

   return ("[bugcheck]");
}

/*****************************************************************************/
/*
UTF-8 null string is munged into an obvious character.
*/

static void GeoLocateMungeUtf8 (char* buf)

{
   char  ch, chr;
   char  *aptr, *cptr, *sptr;

   if (Debug) fprintf (stdout, "GeoLocateMungeUtf8() |%s|\n", buf);

   /* rude and crude */
   if (Debug) fprintf (stdout, "b1 |%s|\n", buf);
   aptr = sptr = buf;
   while (*aptr)
   {
      if (*aptr & 0x80)
      {
         /* unicode point */
         aptr++;
         *sptr++ = *GeoSubsChar;
         if ((*aptr & 0xF0) < 0xE0)
            aptr++;
         else
         if ((*aptr & 0xF0) == 0xE0)
            aptr += 2;
         else
            aptr += 3;
      }
      else
         *sptr++ = *aptr++;
   }
   *sptr = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", buf);
}

/*****************************************************************************/
/*
Unicode escape (\u0000) null string is munged into obvious character.
*/

static void GeoLocateMungeUniEsc (char *buf)

{
   char  ch, chr;
   char  *aptr, *sptr;

   if (Debug) fprintf (stdout, "GeoLocateMungeUniEsc() |%s|\n", buf);
   aptr = sptr = buf;
   while (*aptr)
   {
      /* e.g. "\u00nn" */
      if (*aptr != '\\')
      {
         *sptr++ = *aptr++;
         continue;
      }
      if (*(aptr+1) != 'u')
      {
         *sptr++ = *aptr++;
         *sptr++ = *aptr++;
         continue;
      }
      if (*(aptr+2) != '0' || *(aptr+3) != '0')
      {
         *sptr++ = *GeoSubsChar;
         if (*aptr) aptr++;
         if (*aptr) aptr++;
         if (*aptr) aptr++;
         if (*aptr) aptr++;
         if (*aptr) aptr++;
         if (*aptr) aptr++;
         continue;
      }
      ch = -1;
      aptr += 4;

      /* first hex digit */
      if (chr = *aptr) aptr++;
      if (chr >= '0' && chr <= '9')
         ch = (chr - '0') << 4;
      else
      if (chr >= 'A' && chr <= 'F')
         ch = (chr - 'A') << 4;
      else
      if (chr >= 'a' && chr <= 'f')
         ch = (chr - 'a') << 4;
      else
         ch = -1;

      if (ch > 0)
      {
         /* second hex digit */
         if (chr = *aptr) aptr++;
         if (chr >= '0' && chr <= '9')
            ch += chr - '0';
         else
         if (chr >= 'A' && chr <= 'F')
            ch += chr - 'A';
         else
         if (chr >= 'a' && chr <= 'f')
            ch += chr - 'a';
         else
            ch = -1;
      }
      else
      if (*aptr)
         aptr++;

      /* don't want null or DEL characters */
      if (ch > 0 && ch < 127)
         *(unsigned char*)sptr++ = ch;
      else
         *(unsigned char*)sptr++ = *GeoSubsChar;
   }
   *sptr = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", buf);
}

/*****************************************************************************/
/*
By default the behaviour is non-bursty.
If busty then call GeoLocateNoDelay(1) before processing.
*/

void GeoLocateNoDelay (int enable)

{
   GeoIsNoDelay = enable;
   GeoIsDelayed = !enable;
}

/*****************************************************************************/
/*
By default the behaviour is delayed.  Every call must return a result.
If calls can be missed then call GeoLocateDelayed(0) before processing.
*/

void GeoLocateDelayed (int enable)

{
   GeoIsDelayed = enable;
}

/*****************************************************************************/
/*
By default UTF-8 is (partially) munged into 8 bit ASCII.
If this is not required call this GeoLocateUtf8(1) before processing.
*/

void GeoLocateUtf8 (int enable)

{
   GeoIsUtf8 = enable;
}

/*****************************************************************************/
/*
If |max| is zero return the CacheMax setting.
If the first call is non-zero set that as CacheMax.
*/

int GeoLocateCacheMax (int max)

{
   static int CacheMax = 0;

   /*********/
   /* begin */
   /*********/

   if (!max && !CacheMax) return (CacheMax = GEOLOCATE_CACHE_MAX);
   if (max && !CacheMax)
   {
      if (max < 64) max = 64;
      if (max > 4096) max = 4096;
      return (CacheMax = max);
   }
   return (CacheMax);
}

/*****************************************************************************/
/*
If |logname| is non-NULL store the name and translate and return any value.
If NULL then return the previously set logical name.
*/

char* GeoLocateLogicalName (char *logname)

{
   static char  LogicalName [64] = "GEOLOCATE_SERVICE";

   char  *cptr, *sptr, *zptr;

   if (!logname) return (LogicalName);
   zptr = (sptr = LogicalName) + sizeof(LogicalName)-1;
   for (cptr = logname; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';
   return (GeoTrnLnm (LogicalName, NULL, 0));
}

/*****************************************************************************/
/*
*/

char* GeoLocateSoftwareId ()

{
   return ((char*)SoftwareId);
}

/*****************************************************************************/
/*
Return true if it looks like an IPv4 or IPv6 address.
*/

int GeoIsAddress (char *string)

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoIsAddress() |%s|\n", string);

   if (!string) return (0);

   for (cptr = string; *cptr && isspace(*cptr); cptr++);
   if (!*cptr) return (0);

   cptr = string;
   if (*(unsigned long*)cptr == '::FF' && !memcmp (cptr, "::FFFF:", 7))
      cptr += 7;
   else
   if (*(unsigned long*)cptr == '::ff' && !memcmp (cptr, "::ffff:", 7))
      cptr += 7;

   while (*cptr && (isdigit(*cptr) || *cptr == '.')) cptr++;
   if (!*cptr) return (1);

   for (cptr = string;
        *cptr && (isxdigit(*cptr) || *cptr == ':' || *cptr == '-');
        cptr++);
   if (!*cptr) return (1);

   return (0);
}

/*****************************************************************************/
/*
Translate a logical name using LNM$FILE_DEV.  Returns a pointer to the value
string, or NULL if the name does not exist.  If 'LogValue' is supplied the
logical name is translated into that (assumed to be large enough), otherwise
it's translated into an internal static buffer.  'IndexValue' should be zero
for a 'flat' logical name, or 0..127 for interative translations.
*/

static char* GeoTrnLnm
(
char *LogName,
char *LogValue,
int IndexValue
)
{
   static unsigned short  ValueLength;
   static unsigned long  LnmAttributes,
                         LnmIndex;
   static char  StaticLogValue [256];
   static $DESCRIPTOR (LogNameDsc, "");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(LnmIndex), LNM$_INDEX, &LnmIndex, 0 },
      { sizeof(LnmAttributes), LNM$_ATTRIBUTES, &LnmAttributes, 0 },
      { 255, LNM$_STRING, 0, &ValueLength },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoTrnLnm() |%s| %d\n", LogName, IndexValue);

   LnmIndex = IndexValue;

   LogNameDsc.dsc$a_pointer = LogName;
   LogNameDsc.dsc$w_length = strlen(LogName);
   if (LogValue)
      cptr = LnmItems[2].buf_addr = LogValue;
   else
      cptr = LnmItems[2].buf_addr = StaticLogValue;

   status = sys$trnlnm (0, &LnmFileDevDsc, &LogNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (!(status & 1) || !(LnmAttributes & LNM$M_EXISTS))
   {
      if (Debug) fprintf (stdout, "|(null)|\n");
      return (NULL);
   }

   cptr[ValueLength] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", cptr);
   return (cptr);
}

/****************************************************************************/
/*
Parse the full JSON response into symbols GEOLOCATE__..
*/

#if GEOLOCATE_SYMBOL

void GeoResponseSymbols ()

{
   char  *aptr, *cptr, *sptr, *xptr, *zptr;
   char  buf [256];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GeoResponseSymbols()\n");

   xptr = GeoLocateResponse ("", "", NULL);
   while (aptr = GeoLocateParse (&xptr))
   {
      zptr = (sptr = buf) + sizeof(buf)-1;
      for (cptr = "GEOLOCATE__"; *cptr; *sptr++ = *cptr++);
      for (cptr = aptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      if (*(cptr+1)) cptr++;
      GeoSetSymbol (buf, cptr, 0);
   }
   GeoSetSymbol ("GEOLOCATE__VERSION", SOFTWAREID + sizeof(SOFTWARENM), 0);
}

#endif /* GEOLOCATE_SYMBOL */

/****************************************************************************/
/*
Assign a global symbol.  If the string pointer is null the numeric value is
used.  Symbol lengths are fixed to a maximum of 255.
*/

#if GEOLOCATE_SYMBOL

void GeoSetSymbol
(
char *Name,
char *String,
int Value
)
{
   static char  ValueString [32];
   static $DESCRIPTOR (NameDsc, "");
   static $DESCRIPTOR (ValueDsc, "");
   static $DESCRIPTOR (ValueFaoDsc, "!UL");
   static $DESCRIPTOR (ValueStringDsc, ValueString);

   int  status;
   int  CliSymbolType = GeoSymbolScope;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "GeoSetSymbol() |%s|%s| %d\n", Name, String, Value);

   NameDsc.dsc$a_pointer = Name;
   NameDsc.dsc$w_length = strlen(Name);
   if (!String)
   {
      ValueDsc.dsc$a_pointer = ValueString;
      sys$fao (&ValueFaoDsc, &ValueDsc.dsc$w_length, &ValueStringDsc, Value);
      ValueString[ValueDsc.dsc$w_length] = '\0';
   }
   else
   {
      ValueDsc.dsc$a_pointer = String;
      if ((ValueDsc.dsc$w_length = strlen(String)) > 255)
         ValueDsc.dsc$w_length = 255;
   }

   if (Debug) fprintf (stdout, "|%s| %d\n", Name, ValueDsc.dsc$w_length);

   status = lib$set_symbol (&NameDsc, &ValueDsc, &CliSymbolType);
   if (!(status & 1)) exit (status);
}

#endif /* GEOLOCATE_SYMBOL */

/*****************************************************************************/

