/*****************************************************************************/
/*
                                 NetReject.c

Accept or reject the client connection based on wildcard patterns of either IP
address, CIDR patterns, IP range patterns (n.n.n.n-n.n.n.n) or host name (if
resolved).  To improve matching efficiency the string of accepted/rejected
patterns are converted into null-terminated records by NetRejectSet().

Reports of patterns are copied to allocated memory and converted back into
newline terminated records.  The calling function must deallocate the memory.

A leading character of '#' allows a comment record to be ignored.

A leading character of '$' sets a specific runtime setting (e.g. "$STATUS").

Otherwise a simple wildcard pattern matches either the IP address or host name.

Rejected connections are $LOG optionally recorded in the relevant access log
with an entry similar to the process log entry, like the following:

  wasd.vsm.com.au - - [28/Jul/2023:13:34:21 +1030] "CONNECT \
  /REJECT-129.252.17.1/24~129.252.17.10~wasd.vsm.com.au HTTP/1.0" 418 0

The accept/reject match results are buffered to allow two things; 1) multiple
consecutive matches to be counted, and 2) only a accept/single rejection note
to be output to the server $NOTE process and/or $OPCOM log.

   %HTTPD-I-REJECT, 28-JUL-2023 13:54:58, 2, *.yandex.com \
   95-108-213-154.spider.yandex.com 95.108.213.154 klaatu.lan:443

   %HTTPD-I-REJECT, 28-JUL-2023 14:19:51, 5, 118.123.105.69 klaatu.lan:80

The server shutdown calls this function with the IP address string NULL to
write out any final buffered rejection note. 

$<setting> must occur before any other rules!
----------

$4nn
$5nn
----
Allows a standard HTTP status (e.g. 418, 406, 505) to be added to a list of IP
addresses where connections are immediately dropped.  This allows a complex
WASD_CONFIG_MAP rule to be mapped to the specified status and thereafter any
connections from that IP address immediately dropped, until a purge period
expires.  The syntax is

|# WASD_CONFIG_REJECT
|
|# $4nn <integer-status> <integer-number_of_entries> <integer-purge_minutes>
|$418 250 60

$400
----
When a HTTP/1.n request is significantly malformed the IP address is added to a
list of IP addresses where connections are immediately dropped.  Must follow an
initial $4nn/$5nn directive.  OpenSSL shell command test:

% echo -e "GET / HTTP/1.1\nHost: the.host.name\nTest: \0zero\n\n" | \
openssl s_client -connect the.host.name:443 -ign_eof

$403
----
When a failed authorisation exceeds the failure limit the IP address is added
to a list of IP addresses where connections are immediately dropped.  Must
follow an initial $4nn/$5nn directive.

$DNS
----
Reject connection immediately if host resolution has not occured.
That is, the client host name is the same as the client IP address.

$LOG
----
Reject connection is access logged.

$NOTE
-----
The rejected or accepted connection is noted in the server log.

$OFF
-----
Disable accept or reject processing.

$OPCOM
------
The rejected or accepted connection is reported via OPCOM.


VERSION HISTORY
---------------
15-DEC-2023  MGD  Server Admin / Access Control reporting and editing
21-JUL-2023  MGD  $4nn... extend to NetRejectThisStatus() et.al.
06-JUL-2023  MGD  adapted from ConfigAccept() and ConfigReject() et.al.
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* VMS related header files */
#include <opcdef.h>
#include <ssdef.h>
#include <stsdef.h>

#include <inet.h>

/* application related header */
#include "wasd.h"
#include "gzip.h"

#define WASD_MODULE "NETREJECT"

#define DEFAULT_HTTP_STATUS 418

#define STATUS_SIZE_DEF  250
#define STATUS_SIZE_MAX 1000
#define STATUS_SIZE_MIN  100

/******************/
/* global storage */
/******************/

BOOL  NetAcceptEnabled,
      NetRejectEnabled;

/* used by Request..() */
int  NetRejectStatus400,
     NetRejectStatus403,
     NetRejectStatusCode;

char  NetRejectStatusString [16];

static BOOL  NetAcceptDisabled,
             NetAcceptNote,
             NetAcceptOpcom,
             NetRejectDNS,
             NetRejectDisabled,
             NetRejectLog,
             NetRejectNote,
             NetRejectOpcom;

static int  NetAcceptHostsLength,
            NetAcceptHostsStart,
            NetRejectHostsLength,
            NetRejectHostsStart,
            NetRejectPurgeMinutes,
            NetRejectPurgeSeconds,
            NetRejectStatusActive,
            NetRejectStatusFull,
            NetRejectStatusHit,
            NetRejectStatusLimit,
            NetRejectStatusSize;

static char  *NetConfigAcceptPtr,
             *NetConfigRejectPtr,
             *NetAcceptHostsPtr,
             *NetRejectHostsPtr;

static REJECT_STRUCT  *NetRejectStatusArray;

/********************/
/* external storage */
/********************/

extern int  HttpdTickSecond,
            LoggingEnabled,
            OpcomMessages,
            OpcomTarget;

extern int64  HttpdTime64;

extern uint  SysPrvMask[];

extern char  SoftwareID[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern HTTPD_PROCESS  HttpdProcess;
extern REQUEST_STRUCT  RequestKludge;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Set the global accept/reject hosts data or file name and parse any associated
paramaters.
*/

char* NetRejectInit (char *type)

{
   static char  ErrorBuf [32];

   char  *sptr, *zptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectInit()");

   if (type && *type && to_upper(*type) != 'A' && to_upper(*type) != 'R')
   {
      FaoToBuffer (ErrorBuf, sizeof(ErrorBuf), NULL, "NET=LOAD=!AZ?", type);
      return (ErrorBuf);
   }

   if (!type || to_upper(*type) == 'A')
   {
      /**********/
      /* accept */
      /**********/

      NetConfigAcceptPtr = Config.cfServer.AcceptHostsPtr;
      if (sptr = NetRejectSet ("ACCEPT"))
      {
         MetaConReport (&Config, METACON_REPORT_ERROR, sptr);
         if (!type) return (sptr);
      }
      else
      if (NetAcceptHostsPtr)
      {
         NetAcceptEnabled = true;
         NetAcceptDisabled = false;
         zptr = (sptr = NetAcceptHostsPtr) + NetAcceptHostsLength;
         while (sptr < zptr)
         {
            while (!*sptr && sptr < zptr) sptr++;
            if (sptr >= zptr) break;

            if (*sptr == '#')
            {
               /* comment record, ignored */
               while (*sptr) sptr++;
               continue;
            }

            if (*sptr == '$')
            {
               if (strsame (sptr, "$NOTE", 5))
                  NetAcceptNote = true;
               else
               if (strsame (sptr, "$OFF", 4))
                  NetAcceptDisabled = true;
               else
               if (strsame (sptr, "$OPCOM", 6))
                  NetAcceptOpcom = true;
            }
            else
            {
               /* note where the rules begin */
               NetAcceptHostsStart = sptr - NetAcceptHostsPtr;
               break;
            }

            while (*sptr) sptr++;
            continue;
         }
      }
      else
         NetAcceptDisabled = true;


      FaoToStdout ("%HTTPD-I-ACCEPT, !20%D, !AZ $NOTE:!&B $OPCOM:!&B\n",
                   0, NetAcceptDisabled ? "DISABLED" : "ENABLED",
                   NetRejectNote, NetRejectOpcom);
   }

   if (!type || to_upper(*type) == 'R')
   {
      /**********/
      /* reject */
      /**********/

      NetConfigRejectPtr = Config.cfServer.RejectHostsPtr;
      if (sptr = NetRejectSet ("REJECT"))
      {
         MetaConReport (&Config, METACON_REPORT_ERROR, sptr);
         if (!type) return (sptr);
      }
      else
      if (NetRejectHostsPtr)
      {
         NetRejectEnabled = true;
         NetRejectDisabled = false;
         zptr = (sptr = NetRejectHostsPtr) + NetRejectHostsLength;
         while (sptr < zptr)
         {
            while (!*sptr && sptr < zptr) sptr++;
            if (sptr >= zptr) break;

            if (*sptr == '#')
            {
               /* comment record, ignored */
               while (*sptr) sptr++;
               continue;
            }

            if (*sptr == '$')
            {
               if (strsame (sptr, "$4", 2))
                  NetRejectStatusInit (sptr);
               else
               if (strsame (sptr, "$5", 2))
                  NetRejectStatusInit (sptr);
               else
               if (strsame (sptr, "$DNS", 4))
                  NetRejectDNS = true;
               else
               if (strsame (sptr, "$LOG", 4))
                  NetRejectLog = true;
               else
               if (strsame (sptr, "$NOTE", 5))
                  NetRejectNote = true;
               else
               if (strsame (sptr, "$OFF", 4))
                  NetRejectDisabled = true;
               else
               if (strsame (sptr, "$OPCOM", 6))
                  NetRejectOpcom = true;
            }
            else
            {
               /* note where the rules begin */
               NetRejectHostsStart = sptr - NetRejectHostsPtr;
               break;
            }

            while (*sptr) sptr++;
            continue;
         }
      }
      else
         NetRejectDisabled = true;

      FaoToStdout ("%HTTPD-I-REJECT, !20%D, \
!AZ $DNS:!&B $LOG:!&B $NOTE:!&B $OPCOM:!&B $400:!&B $403:!&B $!UL !UL !UL\n",
                   0, NetRejectDisabled ? "DISABLED" : "ENABLED",
                   NetRejectDNS, NetRejectLog, NetRejectNote, NetRejectOpcom,
                   NetRejectStatus400, NetRejectStatus403, NetRejectStatusCode,
                   NetRejectStatusSize, NetRejectPurgeMinutes);
   }

   return (NULL);
}

/*****************************************************************************/
/*
Set up the $nnn (e.g. $418) parameters.
*/

void NetRejectStatusInit (char *sptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectStatusInit() !AZ", sptr);

   if (strsame (sptr, "$400", 4))
   {
      /* also include significantly malformed requests */
      NetRejectStatus400 = 400;
      return;
   }

   if (strsame (sptr, "$403", 4))
   {
      /* also include authorisation limit exceeds */
      NetRejectStatus403 = 403;
      return;
   }

   memset (&RequestKludge, 0, sizeof(RequestKludge));

   /* initialise */
   NetRejectStatus400 = NetRejectStatus403 = NetRejectStatusCode =
      NetRejectStatusFull = NetRejectStatusHit = NetRejectStatusSize =
      NetRejectPurgeMinutes = NetRejectPurgeSeconds = 0;
   NetRejectStatusString[0] = '\0';
   if (NetRejectStatusArray) VmFree (NetRejectStatusArray, FI_LI);
   NetRejectStatusArray = NULL;

   /* e.g. $<integer> <integer> <integer> */
   NetRejectStatusCode = atoi (sptr+1);
   while (*sptr && !isspace(*sptr)) sptr++;
   while (*sptr && isspace(*sptr)) sptr++;
   NetRejectStatusSize = atoi (sptr);
   while (*sptr && !isspace(*sptr)) sptr++;
   while (*sptr && isspace(*sptr)) sptr++;
   NetRejectPurgeMinutes = atoi (sptr);

   if (NetRejectStatusCode / 100 == 4 ||
       NetRejectStatusCode / 100 == 5)
      sprintf (NetRejectStatusString, "%d", NetRejectStatusCode);
   else
      ErrorNoticed (NULL, SS$_BADPARAM, "$!3UL code",
                    FI_LI, NetRejectStatusCode);

   if (NetRejectStatusSize)
   {
      if (NetRejectStatusSize < STATUS_SIZE_MIN ||
          NetRejectStatusSize > STATUS_SIZE_MAX)
      {
         ErrorNoticed (NULL, SS$_BADPARAM, "$!3UL size !UL (!SL to !SL)",
                       FI_LI, NetRejectStatusCode, NetRejectStatusSize,
                       STATUS_SIZE_MIN, STATUS_SIZE_MIN);
         NetRejectStatusSize = 0;
      }
   }

   if (NetRejectPurgeMinutes)
   {
      /* minimum one minute, maximum one hour */
      if (NetRejectPurgeMinutes < 1 ||
          NetRejectPurgeMinutes > 60)
      {
         ErrorNoticed (NULL, SS$_BADPARAM, "$!3UL purge !UL (!UL to !UL)",
                       FI_LI, NetRejectPurgeMinutes, 1, 60);
         NetRejectPurgeMinutes = 0;
      }
      else
         NetRejectPurgeMinutes = 60;
      NetRejectPurgeSeconds = NetRejectPurgeMinutes * 60;
   }

   if (NetRejectStatusCode && NetRejectPurgeMinutes && NetRejectStatusSize)
   {
      /* an array 1..n+1 */
      NetRejectStatusArray = VmGet (sizeof(REJECT_STRUCT) *
                                    (NetRejectStatusSize+1));
      /* adjust static members of kludged request */
      RequestKludge.rqHeader.HttpVersion = HTTP_VERSION_1_0;
      strzcpy (RequestKludge.rqHeader.MethodName, "CONNECT", 8);
   }
   else
   {
      NetRejectStatus400 = NetRejectStatus403 = NetRejectStatusCode =
         NetRejectStatusSize = NetRejectPurgeMinutes =
         NetRejectPurgeSeconds = 0;
      NetRejectStatusString[0] = '\0';
   }
}

/*****************************************************************************/
/*
Accept the specified connection request by returning false, or reject by true.
*/ 

BOOL NetRejectConnect
(
CLIENT_STRUCT *clptr,
SERVICE_STRUCT *svptr
)
{
   static BOOL  cidr, range;

   int  idx;
   char  *aptr, *cptr, *hnptr, *hsptr, *iaptr, *sptr, *zptr;
   char  buf [256];

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectConnect()");

   /* if neither is defined then just accept */
   if (!NetAcceptHostsPtr && !NetRejectHostsPtr) return (false);

   if (clptr)
   {
      iaptr = clptr->IpAddressString;
      hnptr = clptr->Lookup.HostName;
   }
   else
   {
      /* server shutdown or rule reload */
      NetRejectServerLog (NULL, NULL, "ACCEPT", NULL);
      NetRejectServerLog (NULL, NULL, "REJECT", NULL);
      return (false);
   }

   /******************/
   /* hosts accepted */
   /******************/

   if (NetAcceptHostsPtr && !NetAcceptDisabled)
   {
      aptr = NULL;
      zptr = (sptr = NetAcceptHostsPtr) + NetAcceptHostsLength;
      sptr += NetAcceptHostsStart;
      while (sptr < zptr)
      {
//         if (WATCH_MODULE(WATCH_MOD_NET))
//            WatchThis (WATCHALL, WATCH_MOD_NET, "!&Z", sptr);

         aptr = NULL;
         while (!*sptr && sptr < zptr) sptr++;
         if (sptr >= zptr) break;

         if (*sptr == '#',
             *sptr == '$')
         {
            /* comment record, dollar directives, ignored */
            while (*sptr) sptr++;
            continue;
         }

         aptr = sptr;

         /* look for IPv4 address/CIDR/range in pattern */
         cidr = range = false;
         for (cptr = sptr;
              *cptr == '*' || *cptr == '.' || *cptr == '/'  ||
                              *cptr == '-' || isdigit(*cptr);
              cptr++)
         {
            if (*cptr == '/') cidr = true;
            if (*cptr == '-') range = true;
         }
         if (!*cptr || cidr || range)
            cptr = iaptr;
         else
         {
            /* look for IPv6 address */
            for (cptr = sptr; *cptr && *cptr != ':'; cptr++);
            if (*cptr)
               cptr = iaptr;
            else
               cptr = hnptr;
         }

         if (cidr)
         {
            /* if the IP address matches the CIDR mask */
            if (NetRejectCIDR (cptr, sptr)) break;
         }
         else
         if (range)
         {
            /* if the IP address matches the IP range */
            if (NetRejectRange (cptr, sptr)) break;
         }
         else
         {
            /* if the address/hostname matches the pattern */
            if (StringMatchGreedy (NULL, cptr, sptr)) break;
         }

         while (*sptr) sptr++;
      }
   }
   else
      sptr = zptr = "";

   /* if not at the end of the patterns then don't reject (accept) */
   if (sptr < zptr)
   {
      /************/
      /* accepted */
      /************/

      if (WATCH_CATEGORY (WATCH_CONNECT))
         WatchThis (WATCHALL, WATCH_CONNECT,
                    "ACCEPT !AZ !AZ", sptr, cptr);

      InstanceGblSecIncrLong (&AccountingPtr->ConnectAcceptedCount);

      if (NetAcceptNote) NetRejectServerLog (clptr, svptr, "ACCEPT", aptr);

      return (false);
   }

   /*******************/
   /* status rejected */                                                
   /*******************/

   if (NetRejectStatusSize && !NetRejectDisabled)
   {
      if (idx = NetRejectThisStatus (clptr))
      {
         hsptr = NetRejectStatusArray[idx].HttpStatus;

         if (WATCH_CATEGORY (WATCH_CONNECT))
            WatchThis (WATCHALL, WATCH_CONNECT,
"$!3UL !UL !UL !UL (Active:!UL Limit:!UL Hits:!UL Full:!UL)",
                 NetRejectStatusCode, atoi(hsptr),
                 NetRejectStatusSize, NetRejectPurgeMinutes,
                 NetRejectStatusActive, NetRejectStatusLimit,
                 NetRejectStatusHit, NetRejectStatusFull);

          RequestKludge.rqResponse.HttpStatus = atoi(hsptr);
          RequestHttpStatusCode (&RequestKludge);
          InstanceGblSecIncrLong (&AccountingPtr->ConnectRejectedCount);

          if (NetRejectNote)
             NetRejectServerLog (clptr, svptr, "REJECT", hsptr);
          if (NetRejectLog)
             NetRejectAccessLog (clptr, svptr, hsptr);

          return (true);
      }
   }

   /******************/
   /* hosts rejected */                                                
   /******************/

   if (NetRejectHostsPtr && !NetRejectDisabled)
   {
      aptr = NULL;
      zptr = (sptr = NetRejectHostsPtr) + NetRejectHostsLength;
      sptr += NetRejectHostsStart;
      while (sptr < zptr)
      {
//         if (WATCH_MODULE(WATCH_MOD_NET))
//            WatchThis (WATCHALL, WATCH_MOD_NET, "!&Z", sptr);

         aptr = NULL;
         while (!*sptr && sptr < zptr) sptr++;
         if (sptr >= zptr) break;

         if (*sptr == '#' ||
             *sptr == '$')
         {
            /* comment record, dollar directives, ignored */
            while (*sptr) sptr++;
            continue;
         }

         if (NetRejectDNS)
         {
            /* if the IP address was not resolved */
            if (Config.cfMisc.DnsLookupClient)
               if (strsame (iaptr, hnptr, -1))
               {
                  /* sentinal for failed DNS lookup */
                  aptr = sptr = "?";
                  cptr = iaptr;
                  break;
               }
         }

         aptr = sptr;

         /* look for IPv4 address/CIDR/range in pattern */
         cidr = range = false;
         for (cptr = sptr;
              *cptr == '*' || *cptr == '.' || *cptr == '/'  ||
                              *cptr == '-' || isdigit(*cptr);
              cptr++)
         {
            if (*cptr == '/') cidr = true;
            if (*cptr == '-') range = true;
         }
         if (!*cptr || cidr || range)
            cptr = iaptr;
         else
         {
            /* look for IPv6 address */
            for (cptr = sptr; *cptr && *cptr != ':'; cptr++);
            if (*cptr)
               cptr = iaptr;
            else
               cptr = hnptr;
         }

         if (cidr)
         {
            /* if the IP address matches the CIDR mask */
            if (NetRejectCIDR (cptr, sptr)) break;
         }
         else
         if (range)
         {
            /* if the IP address matches the IP range */
            if (NetRejectRange (cptr, sptr)) break;
         }
         else
         {
            /* if the address/hostname matches the pattern */
            if (StringMatchGreedy (NULL, cptr, sptr)) break;
         }

         while (*sptr) sptr++;
      }
   }
   else
      sptr = zptr = "";

   /* if at the end of the patterns then accept */
   if (sptr >= zptr) return (false);
 
   /************/
   /* rejected */
   /************/

   if (WATCH_CATEGORY (WATCH_CONNECT))
      WatchThis (WATCHALL, WATCH_CONNECT,
                 "REJECT !AZ !AZ", sptr, cptr);

   RequestHttpStatusCode (&RequestKludge);
   InstanceGblSecIncrLong (&AccountingPtr->ConnectRejectedCount);

   if (NetRejectNote) NetRejectServerLog (clptr, svptr, "REJECT", aptr);
   if (NetRejectLog) NetRejectAccessLog (clptr, svptr, aptr);

   return (true);
}

/*****************************************************************************/
/*
Provide a server process log entry for either ACCEPT or REJECT log.
Also provide it as an OPCOM message.
*/

char* NetRejectServerLog
(
CLIENT_STRUCT *clptr,
SERVICE_STRUCT *svptr,
char *type,
char *rule
)
{
   /* index zero is 'accept', index one is 'reject' */
   static int  count[2];
   static int64  time64 [2];
   static char  buffer [3][256];

   BOOL  opcomit, noteit;
   int  idx;
   char  *cptr, *hnptr, *iaptr, *sptr, *zptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectServerLog() !&Z !&Z",
                 type, rule);

   if (clptr)
   {
      iaptr = clptr->IpAddressString;
      hnptr = clptr->Lookup.HostName;
   }

   if (to_upper(*type) == 'A')
   {
      idx = 0;
      noteit = NetAcceptNote;
      opcomit = NetAcceptOpcom;
   }
   else
   if (to_upper(*type) == 'R')
   {
      idx = 1;
      noteit = NetRejectNote;
      opcomit = NetRejectOpcom;
   }
   else
      idx = 2;

   if (cptr = rule)
   {
      sptr = buffer[idx];
      if (*cptr == '?')
      {
         /* looking for unresolved IP address in buffer */
         if (strsame (iaptr, sptr + 2, -1))
         {
            /* hit unresolved IP address */
            count[idx]++;
            return (NULL);
         }
      }
      else
      if (strsame (cptr, sptr, -1))
      {
         /* same pattern */
         count[idx]++;
         return (NULL);
      }
   }

   /**********/
   /* report */
   /**********/

   /* when getting most recent reject entry */
   if (idx == 2)
   {
      /* four null-terminated strings: "rule\0host-name\0ip-addr\0service\0" */
      if (!buffer[1][0]) return (NULL);
      zptr = (sptr = buffer[2]) + sizeof(buffer[2])-4;
      FaoToBuffer (buffer[2], sizeof(buffer[2]), NULL,
                   "!20%D, !UL, ", &time64[1], count[1]);
      while (*sptr) sptr++;
      for (cptr = buffer[1]; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr++ = ' ';
      for (cptr++; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr++ = ' ';
      for (cptr++; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (*cptr)
      {
         /* last string can be empty i.e. "rule\0ip-addr\0service\0\0"  */
         *sptr++ = ' ';
         for (cptr++; *cptr; *sptr++ = *cptr++);
      }
      *sptr++ = '\0';
      return (buffer[2]);
   }

   /* four null-terminated strings: "rule\0host-name\0ip-addr\0service\0" */
   cptr = sptr = buffer[idx];
   /* convert nulls to spaces */
   while (*sptr) sptr++;
   *sptr++ = ' ';
   while (*sptr) sptr++;
   *sptr++ = ' ';
   while (*sptr) sptr++;
   /* last string can be empty i.e. "rule\0ip-addr\0service\0\0"  */
   if (*(sptr+1)) *sptr++ = ' ';

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "!UL !&Z", count[idx], cptr);

   /* step over "? host-name\0\0" */
   if (*cptr == '?') cptr += 2;

   if (noteit)
      if (time64[idx])
         FaoToStdout ("%HTTPD-I-!AZ, !20%D, !UL, !AZ\n",
                      type, &time64[idx], count[idx], cptr);

   if (opcomit)
      if (time64[idx])
         if (OpcomMessages & OPCOM_HTTPD)
            FaoToOpcom ("%HTTPD-I-!AZ, !20%D, !UL, !AZ",
                        type, &time64[idx], count[idx], cptr);

   /************************/
   /* buffer the new entry */
   /************************/

   if (!rule)
   {
      /* shutdown/reload flush of buffer */
      buffer[0][0] = buffer[1][0] = buffer[2][0] = '\0';
      count[0] = count[1] = 0;
      time64[0] = time64[1] = 0;
      return (NULL);
   }

   /* both buffers are the same size */
   zptr = (sptr = buffer[idx]) + sizeof(buffer[0])-6;
   /* copy the rule into the buffer and terminate */
   for (cptr = rule; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr++ = '\0';
   /* if the host name was not resolved */
   if (*rule == '?')
   {
      /* copy the IP address string into the buffer and terminate */
      for (cptr = iaptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr++ = '\0';
   }
   else
   {
      /* copy the host name into the buffer and terminate */
      for (cptr = hnptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr++ = '\0';
      /* copy the IP address string into the buffer and terminate */
      for (cptr = iaptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr++ = '\0';
   }
   /* copy the service into the buffer */
   for (cptr = svptr->ServerHostPort; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *(USHORTPTR)sptr = '\0\0';

   time64[idx] = HttpdTime64;
   count[idx] = 1;

   return (NULL);
}

/*****************************************************************************/
/*
Provide an access log entry for a reject.
*/ 

void NetRejectAccessLog
(
CLIENT_STRUCT *clptr,
SERVICE_STRUCT *svptr,
char *rule
)
{
   char  *cptr, *hnptr, *iaptr, *sptr, *zptr;
   char  buf [256];

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectAccessLog()");

   if (!LoggingEnabled) return;

   iaptr = clptr->IpAddressString;
   hnptr = clptr->Lookup.HostName;

   /* adjust dynamic members of kludged request */
   RequestKludge.ClientPtr = clptr;
   RequestKludge.ServicePtr = svptr;
   RequestKludge.rqTime.BeginTime64 = HttpdTime64;
   sys$numtim (&RequestKludge.rqTime.BeginTime7, &HttpdTime64);
   RequestKludge.rqHeader.RequestUriPtr = buf;

   if (WATCH_CATEGORY (WATCH_LOG))
      RequestKludge.WatchItem = ++Watch.ItemCount;
   else
      RequestKludge.WatchItem = 0;

   zptr = (sptr = buf) + sizeof(buf)-1; 
   for (cptr = "/REJECT~"; *cptr && sptr < zptr; *sptr++ = *cptr++);

   if (*rule == '?')
   {
      /* if failed host name resolution copy in the IP address */
      for (cptr = iaptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   }
   else
   {
      /* copy in the matching pattern */
      for (cptr = rule; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr) *sptr++ = '~';
      /* if the host name was not resolved */
      if (strsame (iaptr, hnptr, -1))
      {
         /* the pattern must have been for an IP address */
         for (cptr = iaptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      }
      else
      {
         /* the pattern might have been for a host name */
         for (cptr = hnptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
         if (sptr < zptr) *sptr++ = '~';
         for (cptr = iaptr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      }
   }
   *sptr = '\0';

   Logging (&RequestKludge, LOGGING_ENTRY);
}

/*****************************************************************************/
/*
If the address pointed to by |cptr| matches the CIDR mask pointed to by
|sptr| then return true, otherwise not matching returns false.
*/

BOOL NetRejectCIDR
(
char *cptr,
char *sptr
)
{
   static char  CidrMask [32];
   static int  mask, retval;
   static in_addr_t  ip1, ip2;

   char  *aptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "NetRejectCIDR() !AZ !AZ", sptr, cptr);

   retval = inet_aton (cptr, &ip1);
   if (!retval) return (true);

   if (!strsame (sptr, CidrMask, -1))
   {
      for (aptr = sptr; *aptr && *aptr != '/'; aptr++);
      *aptr = '\0';
      retval = inet_aton (sptr, &ip2);
      *aptr++ = '/';
      mask = atoi (aptr);
      mask = ((1 << mask) - 1) << (32 - mask);
      mask = htonl (mask);
      if (retval && mask)
         strzcpy (CidrMask, sptr, sizeof(CidrMask));
      else
         CidrMask[0] = '\0';
   }

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "!UL !&Z ip:!8XL mask:!8XL !8XL !8XL !8XL",
                 retval, cptr, ip1, mask, ip2,
                 ip1 & mask, ip2 & mask);

   if (!retval || !mask) return (true);

   return ((ip1 & mask) == (ip2 & mask));
}

/*****************************************************************************/
/*
If the address pointed to by |cptr| matches the IP address range pointed to by
|sptr| (e.g. "46.148.32.0-46.148.47.255" - i.e. low to high) then return true,
otherwise not within the range returns false.
*/

BOOL NetRejectRange
(
char *cptr,
char *sptr
)
{
   static char  RangeOf [32];
   static int  retval;
   static in_addr_t  ip1, ip2, ip3;

   char  *aptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "NetRejectRange() !AZ !AZ", sptr, cptr);

   retval = inet_aton (cptr, &ip1);
   if (!retval) return (true);
   ip1 = ntohl (ip1);

   if (!strsame (sptr, RangeOf, -1))
   {
      for (aptr = sptr; *aptr && *aptr != '-'; aptr++);
      *aptr = '\0';
      retval = inet_aton (sptr, &ip2);
      *aptr++ = '-';
      if (retval)
      {
         ip2 = ntohl (ip2);
         retval = inet_aton (aptr, &ip3);
         ip3 = ntohl (ip3);
      }
      if (retval)
         strzcpy (RangeOf, sptr, sizeof(RangeOf));
      else
         RangeOf[0] = '\0';
   }

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "!UL !&Z ip1:!8XL !UL ip2:!8XL !UL ip3:!8XL !UL result:!&B",
                 retval, cptr, ip1, ip1, ip2, ip2, ip3, ip3,
                 ip1 >= ip2 && ip1 <= ip3);

   if (!retval) return (true);

   return (ip1 >= ip2 && ip1 <= ip3);
}

/*****************************************************************************/
/*
Return a pointer to a file name if the client access configuration contains a
file specification.  Otherwise return NULL.
*/

char* NetRejectFile (char *aptr)

{
   char  *cptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectFile()");

   if (!aptr || !*aptr) return (NULL);
   cptr = SysTrnLnm (aptr);
   if (!cptr)
   {
      for (cptr = aptr; *cptr && *cptr != '\n' && *cptr != ':'; cptr++);
      if (*cptr)
         cptr = aptr;
      else
         cptr = NULL;
   }
   return (cptr);
}

/*****************************************************************************/
/*
Load the text file content of the file name provided by aptr.
Return a leading null error message as indicated, or a pointer to a
null-terminated string containing the file contents.
Calling routine must accept responsibility for VmFree()ing content data.
*/

char* NetRejectLoad (char *aptr)

{
   int  length, status;
   char  *cptr, *sptr;
   ODS_STRUCT  LoadOds;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "NetRejectLoad() !AZ", aptr);

   OdsStructInit (&LoadOds, true);
   sys$setprv (1, &SysPrvMask, 0, 0);
   status = OdsLoadTextFile (&LoadOds, cptr = aptr);
   sys$setprv (0, &SysPrvMask, 0, 0);
   if (VMSnok (status))
   {
      if (status == RMS$_FNF) return (NULL);
      sptr = VmGet (256);
      FaoToBuffer (sptr, 256, NULL, "\0!AZ !&S", aptr, status);
      return (sptr);
   }

   /* prefix a commented file name  */
   length = LoadOds.DataLength + strlen(aptr) + 3;
   sptr = aptr = VmGet (length);
   *sptr++ = '#';
   *sptr++ = ' ';
   while (*cptr) *sptr++ = *cptr++;
   *sptr++ = '\n';
   for (cptr = LoadOds.DataPtr; *cptr; *sptr++ = *cptr++);

   OdsFreeTextFile (&LoadOds);

   return (aptr);
}

/*****************************************************************************/
/*
Using the accept/reject configuration directives load up each newline
terminated record into null terminated.
*/

char* NetRejectSet (char *type)

{
   char  *cptr, *sptr, *zptr;
   ODS_STRUCT  LoadOds;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectSet() !AZ", type);

   if (!type || to_upper(*type) == 'A')
   {
      /**********/
      /* accept */
      /**********/

      NetAcceptHostsStart = 0;
      NetAcceptNote = NetAcceptOpcom = false;

      if (NetAcceptHostsPtr)
      {
         /* free previous memory */
         VmFree (NetAcceptHostsPtr, FI_LI);
         NetAcceptHostsPtr = NULL;
         NetAcceptHostsLength = 0;
      }

      if (NetConfigAcceptPtr)
      {
         /* flush any existing notification records */
         NetRejectServerLog (NULL, NULL, "ACCEPT", NULL);

         if (cptr = NetRejectFile (NetConfigAcceptPtr))
         {
            /* logical name or file specification */
            if (cptr = NetRejectLoad (cptr))
            {
               if (!*cptr) return (cptr+1);
               NetAcceptHostsPtr = cptr;
               NetAcceptHostsLength = strlen(cptr);
            }
         }

         if (!cptr)
         {
            /* use literal string */
            NetAcceptHostsLength = strlen(NetConfigAcceptPtr);
            NetAcceptHostsPtr = VmGet (NetAcceptHostsLength+1);
            strzcpy (NetAcceptHostsPtr,
                     Config.cfServer.AcceptHostsPtr,
                     NetAcceptHostsLength+1);
         }
   
         if (WATCH_MODULE(WATCH_MOD_NET))
            WatchThis (WATCHALL, WATCH_MOD_NET, "!AZ",
                       NetAcceptHostsPtr);

         /* convert to null-terminated records */
         for (sptr = NetAcceptHostsPtr; *sptr; sptr++)
            if (*sptr == '\n') *sptr = '\0';
      }
   }

   if (!type || to_upper(*type) == 'R')
   {
      /**********/
      /* reject */
      /**********/

      NetRejectHostsStart = 0;
      NetRejectNote = NetRejectOpcom = NetRejectLog = NetRejectDNS = false;

      if (NetRejectHostsPtr)
      {
         /* free previous memory */
         VmFree (NetRejectHostsPtr, FI_LI);
         NetRejectHostsPtr = NULL;
         NetRejectHostsLength = 0;
      }

      if (NetConfigRejectPtr)
      {
         /* flush any existing notification records */
         NetRejectServerLog (NULL, NULL, "REJECT", NULL);

         if (cptr = NetRejectFile (NetConfigRejectPtr))
         {
            /* logical name or file specification */
            if (cptr = NetRejectLoad (cptr))
            {
               if (!*cptr) return (cptr+1);
               NetRejectHostsPtr = cptr;
               NetRejectHostsLength = strlen(cptr);
            }
         }

         if (!cptr)
         {
            /* use literal string */
            NetRejectHostsLength = strlen(NetConfigRejectPtr);
            NetRejectHostsPtr = VmGet (NetRejectHostsLength+1);
            strzcpy (NetRejectHostsPtr,
                     Config.cfServer.RejectHostsPtr,
                     NetRejectHostsLength+1);
         }
   
         if (WATCH_MODULE(WATCH_MOD_NET))
            WatchThis (WATCHALL, WATCH_MOD_NET, "!AZ",
                       NetRejectHostsPtr);

         /* convert to null-terminated records */
         for (sptr = NetRejectHostsPtr; *sptr; sptr++)
           if (*sptr == '\n') *sptr = '\0';
      }
   }

   return (NULL);
}

/*****************************************************************************/
/*
Check the array of IP addresses for a match.
If found return the (non-zero) index number, otherwise zero. 
*/ 

int NetRejectThisStatus (CLIENT_STRUCT *clptr)

{
   int  idx;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectThisStatus() !AZ !&I",
                 clptr->IpAddressString, &clptr->IpAddress);

   if (!NetRejectStatusSize) return (0);

   if (idx = NetRejectCheckStatus (clptr))
   {
      NetRejectStatusHit++;
      NetRejectStatusArray[idx].HitCount++;
      NetRejectStatusArray[idx].TickSecond = HttpdTickSecond +
                                             NetRejectPurgeSeconds;
   }
   return (idx);
}

/*****************************************************************************/
/*
Check the array for a match.  If found return the (non-zero) index number.
*/ 

int NetRejectCheckStatus (CLIENT_STRUCT *clptr)

{
   int  idx;
   IPADDRESS  *ia1ptr,
              *ia2ptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectCheckStatus() !AZ !&I",
                 clptr->IpAddressString, &clptr->IpAddress);

   if (!NetRejectStatusSize) return (0);

   ia1ptr = &clptr->IpAddress;
   for (idx = 1; idx <= NetRejectStatusLimit; idx++)
   {
      ia2ptr = &NetRejectStatusArray[idx].IpAddress;
      if (!ia2ptr->iadr_size) continue;

      if (WATCH_MODULE(WATCH_MOD_NET))
         WatchThis (WATCHALL, WATCH_MOD_NET, "!UL !UL !&I !UL !&I !UL !UL",
                    idx, ia1ptr->iadr_size, ia1ptr, ia2ptr->iadr_size, ia2ptr,
                    NetRejectStatusArray[idx].HitCount,
                    HttpdTickSecond - NetRejectStatusArray[idx].TickSecond);

      if (ia1ptr->iadr_size == 4)
         if (ia2ptr->iadr_size == 4)
            if (*(ULONGPTR)ia1ptr->address == *(ULONGPTR)ia2ptr->address)
               break;
            else
               continue;

      if (ia1ptr->iadr_size == 16)
         if (ia2ptr->iadr_size == 16)
            if (!memcmp (&ia1ptr->address, &ia2ptr->address, 16))
               break;
   }

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "!UL !UL !UL",
                 idx, NetRejectStatusLimit, NetRejectStatusSize);

   if (idx <= NetRejectStatusLimit) return (idx);
   return (0);
}

/*****************************************************************************/
/*
First check the array for a match.  If hit then return the index number.  If
not hit then if the returned index is non-zero add the IP address at that
index, otherwise if zero the array is full and we'll drop the address and just
wait for the next purge.
*/ 

int NetRejectSetStatus (REQUEST_STRUCT *rqptr)

{
   int  idx;
   IPADDRESS  *iaptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectSetStatus() !AZ",
                 rqptr->ClientPtr->IpAddressString);

   if (!NetRejectStatusSize) return (0);

   /* if there is already an entry for this IP address */
   if (idx = NetRejectCheckStatus (rqptr->ClientPtr))
   {
      NetRejectStatusArray[idx].HitCount++;
      NetRejectStatusArray[idx].TickSecond = HttpdTickSecond +
                                             NetRejectPurgeSeconds;
      NetRejectStatusHit++;
   }
   else
   {
      /* look for a purged item */
      for (idx = 1; idx <= NetRejectStatusLimit; idx++)
         if (!NetRejectStatusArray[idx].IpAddress.iadr_size) break;

      /* if no purged item found */
      if (idx >= NetRejectStatusLimit)
      {
         /* if we've run out of space just forget it */
         if (NetRejectStatusLimit + 1 >= NetRejectStatusSize)
         {
            NetRejectStatusFull++;
            return (0);
         }
         idx = ++NetRejectStatusLimit;
         NetRejectStatusActive++;
      }

      /* use the array entry to store this IP address */
      iaptr = &rqptr->ClientPtr->IpAddress;
      memcpy (&NetRejectStatusArray[idx].IpAddress, iaptr, sizeof(IPADDRESS));
      NetRejectStatusArray[idx].HitCount = 1;
      NetRejectStatusArray[idx].TickSecond = HttpdTickSecond +
                                             NetRejectPurgeSeconds;
      sprintf (NetRejectStatusArray[idx].HttpStatus, "%3.3u",
               rqptr->rqResponse.HttpStatus);
   }

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "!UL !&I !UL !UL",
                 idx, &NetRejectStatusArray[idx].IpAddress,
                      NetRejectStatusArray[idx].HitCount,
                      NetRejectStatusArray[idx].TickSecond);

   if (WATCH_CATEGORY (WATCH_CONNECT))
      WatchThis (WATCHALL, WATCH_CONNECT, "REJECT !AZ !AZ",
                 NetRejectStatusArray[idx].HttpStatus,
                 rqptr->ClientPtr->IpAddressString);

   NetRejectServerLog (rqptr->ClientPtr, rqptr->ServicePtr, "REJECT",
                       NetRejectStatusArray[idx].HttpStatus);
   NetRejectAccessLog (rqptr->ClientPtr, rqptr->ServicePtr,
                       NetRejectStatusArray[idx].HttpStatus);

   return (idx);
}

/*****************************************************************************/
/*
Purge all entries older than 'minutes'.
Return the current number of entries in the array.
*/ 

int NetRejectPurgeStatus (int minutes)

{
   int  idx, TickSecond;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET,
                 "NetRejectPurgeStatus() !SL", minutes);

   if (!NetRejectStatusSize) return (0);

   if (!minutes) minutes = NetRejectPurgeSeconds;
   minutes *= 60;

   /* zero the IP address size for anything older than the specified seconds */
   NetRejectStatusActive = 0;
   for (idx = 1; idx <= NetRejectStatusLimit; idx++)
      if (NetRejectStatusArray[idx].TickSecond - minutes <= HttpdTickSecond)
         NetRejectStatusArray[idx].IpAddress.iadr_size = 0;
      else
         NetRejectStatusActive++;

   /* reset the array count to the lowest free array element */
   for (idx = NetRejectStatusLimit; idx > 0; idx--)
      if (NetRejectStatusArray[idx].IpAddress.iadr_size) break;
   NetRejectStatusLimit = idx;

   return (NetRejectStatusLimit);
}

/*****************************************************************************/
/*
Return a pointer to an allocated string containing a string representing the
accept or reject report.  Calling routine must accept responsibility for
VmFree()ing content data.
*/

char* NetRejectReport (char *type)

{
   char  *aptr, *cptr, *sptr, *zptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectReport() !AZ", type);

   if (strsame (type, "ACCEPT", -1))
   {
      /* the loaded accept list */
      cptr = NetAcceptHostsPtr;
      sptr = aptr = VmGet (NetAcceptHostsLength);
      zptr = sptr + NetAcceptHostsLength;
      while (sptr < zptr)
      {
         while (*cptr && sptr < zptr) *sptr++ = *cptr++;
         if (sptr < zptr) *sptr++ = '\n';
         cptr++;
      }
   }
   else
   if (strsame (type, "REJECT", -1))
   {
      /* the loaded reject list */
      cptr = NetRejectHostsPtr;
      sptr = aptr = VmGet (NetRejectHostsLength);
      zptr = sptr + NetRejectHostsLength;
      while (sptr < zptr)
      {
         while (*cptr && sptr < zptr) *sptr++ = *cptr++;
         if (sptr < zptr) *sptr++ = '\n';
         cptr++;
      }
   }
   else
   {
      aptr = VmGet (32);
      strcpy (aptr, "<i>(none)</i>");
   }

   return (aptr);
}

/*****************************************************************************/
/*
From the Server Admin menu generate a report of the rules and (recent) history.
*/ 

void NetRejectReportAdmin
(
REQUEST_STRUCT *rqptr,
char *type
)
{
   static char ReportRulesFao [] =
"<p><table class=\"ctgry\">\n\
<tr><th class=\"ctttl\">Rules</th></tr>\n\
<tr><td>\n\
<table>\n\
<tr><td class=\"pretxt\">!AZ</td></tr>\n\
</table>\n\
</td></tr>\n\
</table>\n";

   static char ReportIpFao [] =
"<p><table class=\"ctgry\">\n\
<tr><th class=\"ctttl\">Current IP</th></tr>\n\
<tr><td>\n\
<table>\n\
<tr class=\"sbttl talft\">\
<th></th>\
<th>Hit</th>\
<th>Expires</th>\
<th>Status</th>\
<th>IP</th></tr>\n";

   static char ReportHistoryFao [] =
"<p><table class=\"ctgry\">\n\
<tr><th class=\"ctttl\">History</th></tr>\n\
<tr><td>\n\
<table>\n\
<tr><td class=\"pretxt\">";

   static char ReportEndFao [] =
"</table>\n\
</td></tr>\n\
</table>\n";

   static char ReportNoneFao [] = "<i>none defined</i>\n";

   char  *sptr;
   char  ScratchString [256];

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectReportAdmin() !&AZ", type);

   if (type) *type = toupper(*type);
   if (!type || (*type != 'A' && *type != 'R'))
   {
      rqptr->rqResponse.HttpStatus = 400;
      ErrorGeneral (rqptr, "Unknown function.", FI_LI);
      AdminEnd (rqptr);
      return;
   }

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   ResponseHeader200 (rqptr, "text/html", NULL);

   FaoToBuffer (ScratchString, sizeof(ScratchString), NULL,
                "Access Control &nbsp;&ndash;&nbsp; !AZ Report", type);
   AdminPageTitle (rqptr, ScratchString);

   if (sptr = NetRejectReport (type))
   {
      FaoToNet (rqptr, ReportRulesFao, sptr);
      /* NetRejectReport() allocated memory needs to be freed */
      VmFree (sptr, FI_LI);
   }
   else
      FaoToNet (rqptr, ReportRulesFao, ReportNoneFao);

   if (strsame (type, "REJECT", -1))
   {
      if (NetRejectStatusSize)
      {
         FaoToNet (rqptr, ReportIpFao);
         NetRejectReportIp (rqptr);
         FaoToNet (rqptr, ReportEndFao);
      }

      FaoToNet (rqptr, ReportHistoryFao);
      NetRejectReportHistory (rqptr);
      FaoToNet (rqptr, ReportEndFao);
   }

   AdminEnd (rqptr);
}

/*****************************************************************************/
/*
Generate a report showing IP addresses currently beinf rejected.  These are
subject to purging.
*/

void NetRejectReportIp (REQUEST_STRUCT *rqptr)

{
   static char ReportIpFao [] =
"<tr><td class=\"targht\">!UL.</td><td class=\"targht\">!UL</td>\
<td class=\"targht\">!UL</td><td>!AZ</td><td>!&I</td></tr>\n";

   static char ReportEmptyFao [] =
"<tr><td></td><td colpsan=\"4\"><i>empty</i></td></tr>\n";

   int  count, idx, minutes, status;
   ulong  FaoVector [8];
   ulong  *vecptr;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectReportIp()");

   count = 0;
   for (idx = 1; idx <= NetRejectStatusLimit; idx++)
   {
      if (!NetRejectStatusArray[idx].IpAddress.iadr_size) continue;
      minutes = NetRejectStatusArray[idx].TickSecond - HttpdTickSecond;
      minutes = (minutes + 65) / 60;
      if (minutes <= 0)
      {
         /* if reached timeout might as well time it out */
         NetRejectStatusArray[idx].IpAddress.iadr_size = 0;
         continue;
      }
      vecptr = FaoVector;
      *vecptr++ = ++count;
      *vecptr++ = NetRejectStatusArray[idx].HitCount;
      *vecptr++ = minutes;
      *vecptr++ = NetRejectStatusArray[idx].HttpStatus;
      *vecptr++ = &NetRejectStatusArray[idx].IpAddress;
      status = FaolToNet (rqptr, ReportIpFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
   }
   if (!count) FaoToNet (rqptr, ReportEmptyFao);
}

/*****************************************************************************/
/*
Generate a (recent) history by opening and reading relevant records from the
current operator log.  Relies on the $NOTE rule being enabled.
*/

void NetRejectReportHistory (REQUEST_STRUCT *rqptr)

{
   static char ReportEmptyFao [] = "<i>empty</i>\n";

   int  count, status;
   char  *sptr;
   char  buf [1024];
   ODS_STRUCT  HistoryOds;
   ODS_STRUCT  *odsptr = &HistoryOds;

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

   if (WATCH_MODULE(WATCH_MOD_NET))
      WatchThis (WATCHALL, WATCH_MOD_NET, "NetRejectReportHistory()");

   if (!HttpdProcess.SysOutputFile)
   {
      FaoToNet (rqptr, "!&S\n", SS$_NOSUCHFILE);
      return;
   }

   /* Q&D flush SYS$OUTPUT */
   fsync (STDOUT_FILENO);

   OdsStructInit (odsptr, true);
   status = OdsOpenShrUpdReadOnly (odsptr, HttpdProcess.SysOutput, false, true);
   if (VMSnok (status))
   {
      FaoToNet (rqptr, "!&S\n", status);
      return;
   }

   count = 0;

   for (;;)
   {
      odsptr->Rab.rab$l_ubf = buf;
      odsptr->Rab.rab$w_usz = sizeof(buf) - 1;
      status = sys$get (&odsptr->Rab, 0, 0);
      if (VMSnok (status)) break;
      buf[odsptr->Rab.rab$w_rsz++] = '\n';
      if (!MATCH16 (buf, "%HTTPD-I-REJECT,")) continue;
      NetWriteBuffered (rqptr, NULL, buf + 17, odsptr->Rab.rab$w_rsz - 17);
      count++;
   }

   sys$close (&odsptr->Fab, 0, 0);

   if (status == RMS$_EOF) status = SS$_NORMAL;
   if (VMSnok (status))
   {
      FaoToNet (rqptr, "!&S\n", status);
      return;
   }

   /* get anything lurking in the most recent reject entry */
   sptr = NetRejectServerLog (NULL, NULL, "", NULL);
   if (sptr)
   {
      NetWriteBuffered (rqptr, NULL, sptr, -1);
      count++;
   }

   if (!count) FaoToNet (rqptr, ReportEmptyFao);
}

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

