/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                               qdLogStats.c

Quick & Dirty LOG STATisticS.

Utility to extract very elementary statistics from Web server common/combined
format log files.  A number of filters allow subsets of the log contents to be
selected.

If a filter pattern does not begin with the '^' character then it is considered
to be a wildcard pattern.  If the pattern begins with the '^' character then it
is considered to be regular expression.  A filter pattern result can be negated
by the use of a leading '!' character.  If the negation is used then the target
string cannot be empty.  If it's empty it always treated as no match (and
filtered out). 


WILDCARD EXPRESSIONS
--------------------
A wildcard expression uses the '*' to match any zero or more characters, the
'%' to match any one character, the '?' to match any zero or one character. 
The '*', '%' and '?' are translated into an appropriate regular expression,
with the start and end of the string anchored.  This should act much the same
as previous versions of QDLOGSTATS.  If you want to do anything more complex
than simple wildcard matching you will need to use a regular expression.

The following expression would match all requests with a path that was rooted
at "/ht_root/".

  /ht_root/*

This expression matches all paths that accessed the Alpha object files in the
WASD source (and build) tree.

  /ht_root/*/obj_axp/*


REGULAR EXPRESSIONS
-------------------
Are provided using the GNU extended regular expression matching and search
library, version 0.12 (from RX1.5 kit).  Copyright (C) 1993 Free Software
Foundation, Inc.  Although a full explanation of regular expression matching is
well beyond the scope of this source code description here's a quick mnemonic.

  \         Quote the next metacharacter
  ^         Match the beginning of the line
  .         Match any character (except newline)
  $         Match the end of the line
  |         Alternation (or)
  [abc]     Match only a, b or c
  [^abc]    Match anything except a, b and c
  [a-z0-9]  Match any character in the range a to z or 0 to 9
 
  *         Match 0 or more times
  +         Match 1 or more times
  ?         Match 1 or 0 times
  {n}       Match exactly n times
  {n,}      Match at least n times
  {n,m}     Match at least n but not more than m times
 
  Match-self Operator                 Ordinary characters.
  Match-any-character Operator        .
  Concatenation Operator              Juxtaposition.
  Repetition Operators                *  +  ? {}
  Alternation Operator                |
  List Operators                      [...]  [^...]
  Grouping Operators                  (...)
  Back-reference Operator             \digit
  Anchoring Operators                 ^  $

More detailed explanations abound.  Keep in mind that this program uses the
library in case-insensitive, extended Posix mode (basically EGREP look-alike).

The following expression would match all requests with a path that was rooted
at "/ht_root/".

  ^^/ht_root/.*

This expression matches all paths that accessed the Alpha object files in the
WASD source (and build) tree.

  ^^/ht_root/.*/obj_axp/.*

Note the leading '^' which directs the program that this is a regular
expression.


OTHER CONSIDERATIONS
--------------------
Matching log file records can be viewed as processed using the
/VIEW=[type] qualifier, where <type> is ALL, MATCH (default) or NOMATCH.

For fields that are commonly URL-encoded (or for any field that can usefully be
URL-encoded to disguise the actual path) the /DECODE qualifier specifies that
the field should be URL-decoded before filter matching.  If the /DECODE
qualifier is used without keywords all three of these fields are decoded,
otherwise any combination of PATH, QUERY and REFERER may be used to selectively
decode that field.  For example /DECODE=(PATH,QUERY).  Remember that decoding
uses CPU cycles and adds processing time.

A progress indicator can be displayed using the /PROGRESS qualifier.  With this
a "+" is output for each file processed and a "." for each 1000 records in any
file.  An optional integer can be provides as /PROGRESS=integer to change this
record value.

Note that all selections are based on string matching.  Files and/or records
cannot be selected on the basis of being before or after a particular date for
instance.  Also that records are accepted or rejected in the order in which the
results are displayed, that is if a record is rejected against method it will
never be assessed against user-agent ... UNLESS then /ALL qualifier is applied. 
When /ALL is used comparison continues against all filter strings even if one
or more do not match.  This is useful for displaying the totals that match each
of multiple filter critera (not much use against a single one).

If /LOOKUP[=<integer>] is enabled then literal client host addresses have their
names resolved (if possible).  This name is displayed when log line is output. 
When enabled the resolved name is also used when matching against client hosts.

Although part of the WASD package there is no reason why this shouldn't be of
use with other packages if desired.


EXAMPLES
--------
Requires a foreign verb or such (shorter version might save keystrokes :^)

  $ QDLOG == "$CGI-BIN:[000000]QDLOGSTATS"
  $ QDLOGSTATS == "$CGI-BIN:[000000]QDLOGSTATS"

1)  Records from September 1999.

    $ QDLOGSTATS HT_LOGS:*1999*.LOG /DATE="*/SEP/1999*"

2)  Records where the browser was an X-based Netscape Navigator

    $ QDLOGSTATS HT_LOGS:*.LOG /USERAGENT=*MOZILLA*X11*

3)  Records of POST method requests

    $ QDLOGSTATS HT_LOGS:*.LOG /METHOD=POST

4)  Records requesting a particular path

    $ QDLOGSTATS HT_LOGS:*.LOG /PATH="/cgi-bin/*"

5)  Records with a query string 

    $ QDLOGSTATS HT_LOGS:*.LOG /QUERY="%*"
    $ QDLOGSTATS HT_LOGS:*.LOG /QUERY="^^.+$"

6) Records without a query string (or without a specific query string)

    $ QDLOGSTATS HT_LOGS:*.LOG /QUERY="^^$"
    $ QDLOGSTATS HT_LOGS:*.LOG /QUERY="!*grotty*"
    $ QDLOGSTATS HT_LOGS:*.LOG /QUERY="!^grotty"

7)  Records requesting a particular path

    $ QDLOGSTATS HT_LOGS:*.LOG /PATH="/cgi-bin/*"

8)  Select proxy records requesting (a) particular site(s)

    $ QDLOGSTATS HT_LOGS:*8080*.LOG /PATH="http://*.compaq.com*"
    $ QDLOGSTATS HT_LOGS:*8080*.LOG /METHOD=POST /PATH="^http://.*sex.*/.*"

9)  Records where the request was authenticated

    $ QDLOGSTATS HT_LOGS:*.LOG /AUTHUSER="%*"
    $ QDLOGSTATS HT_LOGS:*.LOG /AUTHUSER="^.+"
    $ QDLOGSTATS HT_LOGS:*.LOG /AUTHUSER="DANIEL"

10) Records where the request was authenticated, excluding DANIEL

    $ QDLOGSTATS HT_LOGS:*.LOG /AUTHUSER="!DANIEL"

11) Records with requests originating from (a) particular host(s)

    $ QDLOGSTATS HT_LOGS:*.LOG /CLIENT=131.185.250.1
    $ QDLOGSTATS HT_LOGS:*.LOG /LOOKUP /CLIENT="*.vsm.com.au"


CGI INTERFACE
-------------
A CGI interface is available.  It basically parallels the CLI behaviour
described above. 

  http://the.host.name/cgi-bin/qdlogstats

So that it's not available for uncontrolled use the script *must* be subject to
authorization (i.e. have a non-empty REMOTE_USER).  For the WASD package this
may be enabled using the following HTTPD$AUTH rule.

  [whatever-realm]
  /cgi-bin/qdlogstats r+w,~siteadmin

For VMS Apache (CSWS) using SYSUAF authentication this might be configured
using the following entries in [CONF]HTTPD.CONF

  LoadModule auth_openvms_module
  /apache$common/modules/mod_auth_openvms.exe_alpha

  <Location /cgi-bin/qdlogstats>
  AuthType Basic
  AuthName "OpenVMS authentication"
  AuthUserOpenVMS On
  require valid-user
  </Location>

If there seems to be no relevant CGI variables the script displays a request
form that allows the entering of strings against the various fields of the log
file entries.  This form uses the GET method (putting form elements into the
query string) and so searches commonly or periodically made can be saved as
bookmarks if desired.

The account processing the script must have access to the log directory and
files.  This may be provided by the server account itself (i.e. the logs are
normally accessable) or the scripting account itself has access.  For instance,
with WASD on VMS V6.2 or later this could be provided using the /PERSONA
functionality and a HTTPD$MAP rule such as

  set /cgi-bin/qdlogstats script=as=SYSTEM

An alternative is to install the script executable with sufficient privileges
to access the files.  Normally this would only be SYSPRV but as the script
attempts to enable READALL as well so it *could* be installed with that if
necessary.  The following example DCL executed during server startup would
ensure such access.

  $ INSTALL ADD /PRIVILEGE=READALL CGI-BIN:[000000]QDLOGSTATS.EXE

For WASD 8.1 and later access control should not need to be changed.  For
non-WASD and WASD earlier than 8.1 the following should be applied.  If this is
provided it would also be prudent to control access to the executable from
arbitrary accounts at the CLI, and restricting it to the scripting account
using an ACL.  For example

  $ SET SECURITY CGI-BIN:[000000]QDLOGSTATS.EXE -
        /ACL=((IDENT=HTTP$SERVER,ACCESS=R+E),(IDENT=*,ACCESS=NONE)

The CGI version REQUIRES the following system-level logical name to locate the
log files for it.  If this name does not exist the CGI interface will not work. 
All log files must be available via this logical.  The logical name is defined
/SYSTEM to limit a possibly READALL privilege being used against arbitrary
paths.

  $ DEFINE /SYSTEM QDLOGSTATS_LOGS "HT_LOGS:"


QUALIFIERS
----------
/ALL                  compare all records to all filters
/AUTHUSER=            pattern (any authenticated username)
/BEFORE=              files (not records!) modified before VMS time
/CLIENT=              pattern (client host name or IP address)
/DATETIME=            pattern ("11/Jun/1999:14:08:49 +0930")
/DBUG                 turns on all "if (Debug)" statements
/DECODE[=keyword]     URL-decode PATH, QUERY and/or REFERER before filtering
/IP=<integer>         4 or 6 to select either IPv4 or IPv6 records
/LOG                  show the name of the log file the records come from
/LOOKUP[=<integer>]   lookup host name from literal address
/METHOD=              HTTP method ("GET", "POST", "HEAD", etc.)
/OUTPUT=              file specification
/PATH=                pattern (URL path component only)
/PROGRESS[=integer]   show progress during processing
/PROTOCOL=            HTTP protocol ("HTTP/1.0" or "HTTP/1.1") as a string
/QUERY=               pattern (URL query component only)
/REFERER=             pattern (HTTP "Referer:" field, COMBINED only)
/REMOTEID=            pattern (anything in the RFC819 remote ident field)
/RESPONSE=            HTTP response status code pattern
/SINCE=               files (not records!) modified since VMS time
/SIZE=([MIN=|MAX=])   minimum and/or maximum response size  
/SOFTWAREID           (and /VERSION) display QDLOGSTATS and CGILIB versions
/USERAGENT=           pattern (HTTP "User-Agent:", COMBINED only)
/VIEW[=type]          display matching records, where <type> can be
                      ALL, MATCH (default) or NOMATCH


BUILD DETAILS
-------------
See BUILD_QDLOGSTATS.COM procedure.


COPYRIGHT
---------
Copyright (C) 2000-2004 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.

Copyright (C) 1993 Free Software Foundation, Inc.
Extended regular expression matching and search library, version 0.12.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
11-MAY-2004  MGD  v1.10.0, IPv6 modifications
                           /IP=<integer> to select either IPv4 or IPv6 records
23-DEC-2003  MGD  v1.9.2, minor conditional mods to support IA64
02-DEC-2003  MGD  v1.9.1, 206 partial content
12-AUG-2003  MGD  v1.9.0, HTTP protocol filter
17-MAY-2003  MGD  v1.8.0, use regular expression matching (GNU RX1.5 source)
                          (this fundamentally changes some previous behaviours)
                          allow for protocol in URL ("method path protocol")
02-JAN-2003  MGD  v1.7.0, filtering on HTTP response status and response size
24-NOV-2002  MGD  v1.6.0, URL-decoding path/query/referer before filtering,
                          check for account SYSPRV before CLI activating
05-NOV-2002  MGD  v1.5.1, extend CGI log search to check for QDLOGSTATS_LOGS
                          then to fallback to HTTPD$LOG and HT_LOGS logicals
26-SEP-2002  MGD  v1.5.0, make CGI log search dependent on the QDLOGSTATS_LOGS
                          logical name being defined and log files within that
16-AUG-2002  MGD  v1.4.0, add log file last modification before/since,
                          some accomodations for CSWS 1.2,
                          bugfix; silly boy - what about HEAD method!
30-MAR-2002  MGD  v1.3.0, accumulate request methods
01-JUL-2001  MGD  v1.2.2, add 'SkipParameters' for direct OSU support
30-JAN-2001  MGD  v1.2.1, small improvement to lookup cache for noting
                          addresses that never resolve (and do it slowly)
10-JAN-2001  MGD  v1.2.0, lookup client host name
23-DEC-2000  MGD  v1.1.0, CGI interface (still pretty q&d)
14-NOV-2000  MGD  v1.0.0, initial development
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#define SOFTWAREVN "1.10.0"
#define SOFTWARENM "QDLOGSTATS"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <jpidef.h>
#include <prvdef.h>
#include <stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>
#include <unixlib.h>

/* VMS-related header files */
#include <descrip.h>
#include <iodef.h>
#include <lnmdef.h>
#include <lib$routines.h>
#include <prvdef.h>
#include <rms.h>
#include <ssdef.h>
#include <starlet.h>
#ifdef __VAX
   /* "Compaq C V6.4-005 on OpenVMS VAX V7.3" seems to need this */
#  define SYS$GETMSG sys$getmsg
   int sys$getmsg (__unknown_params);
#endif
#include <stsdef.h>
#include <syidef.h>

/* TCP/IP-related header files */
#include <in.h>
#include <netdb.h>
#include <inet.h>

/* application related header files */
#include <cgilib.h>
#include <regex.h>

/* regular expression processing */
#define REGEX_CHAR '^'
/* compile flags, case-insensitive extended GREP */
#define REGEX_C_FLAGS (REG_EXTENDED | REG_ICASE)
/* execution flags */
#define REGEX_E_FLAGS (0)
/* one for each of the possible unique FilterThisOut()s */
#define REGEX_PATTERN_MAX 15

#define BOOL int
#define true 1
#define false 0

typedef struct _IO_SB
{
   unsigned short  Status;
   unsigned short  Count;
   unsigned long  Unused;
} IO_SB;

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define FI_LI "QDLOGSTATS",__LINE__

/* used in modulus for every so many records progress */
#define PROGRESS_RECORDS 1000
/* wrap every so many characters */
#define PROGRESS_WRAP_CLI 80
#define PROGRESS_WRAP_CGI 60

/* maximum log line/record length */
#define MAX_LINE_LENGTH 2048

#define DEFAULT_LOGS_LOGICAL "QDLOGSTATS_LOGS"
#define DEFAULT_LOGS2_LOGICAL "HTTPD$LOG"
#define DEFAULT_LOGS3_LOGICAL "HT_LOGS"
#define DEFAULT_LOGS_FILESPEC "*.LOG*;"
#define DEFAULT_LOGS_DIR_APACHE "APACHE$SPECIFIC:[LOGS]"
#define DEFAULT_LOGS_APACHE "*ACCESS_LOG*.;"
#define DEFAULT_LOGS_DIR_OSU  "WWW_ROOT:[SYSTEM]"
#define DEFAULT_LOGS_OSU "ACCESS*.LOG;"
#define DEFAULT_LOGS_DIR_WASD "HT_LOGS:"
#define DEFAULT_LOGS_WASD "*ACCESS*.LOG*;"

#define VIEW_NONE     0
#define VIEW_LOG      1
#define VIEW_ALL      2
#define VIEW_MATCH    3
#define VIEW_NOMATCH  4

char  CopyrightInfo [] =
"Copyright (C) 2000-2004 Mark G.Daniel.\n\
This software comes with ABSOLUTELY NO WARRANTY.\n\
This is free software, and you are welcome to redistribute it\n\
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.";

char  Utility [] = "QDLOGSTATS";

BOOL  Debug,
      DoCgiStats,
      DoCliStats,
      FilterOnAll,
      ShowLogFile,
      UrlDecodePath,
      UrlDecodeQuery,
      UrlDecodeReferer;

int  AccessProblemCount,
     AuthUserFilterAcceptCount,
     AuthUserFilterRejectCount,
     ClientFilterAcceptCount,
     ClientFilterRejectCount,
     CtimeBefore,
     CtimeSince,
     CombinedLogRecordCount,
     CommonLogRecordCount,
     DateTimeFilterRejectCount,
     DateTimeFilterAcceptCount,
     FileCountTotal,
     Ip4FilterAcceptCount,
     Ip4FilterRejectCount,
     Ip6FilterAcceptCount,
     Ip6FilterRejectCount,
     IpVersionFilter,
     LastModifiedIgnoredCount,
#define TCPIP_LOOKUP_HOST_NAME_RETRY LookupRetry
     LookupRetry,
     MethodFilterAcceptCount,
     MethodFilterRejectCount,
     MethodConnectCount,
     MethodDeleteCount,
     MethodGetCount,
     MethodHeadCount,
     MethodPostCount,
     MethodPutCount,
     MethodUnknownCount,
     PathFilterAcceptCount,
     PathFilterRejectCount,
     ProgressCount,
     ProgressWrap,
     ProtocolFilterAcceptCount,
     ProtocolFilterRejectCount,
     QueryFilterAcceptCount,
     QueryFilterRejectCount,
     RecordCount,
     RecordCountTotal,
     SuspectRecordCountTotal,
     RequestCountTotal,
     RefererFilterAcceptCount,
     RefererFilterRejectCount,
     RemoteIdentFilterAcceptCount,
     RemoteIdentFilterRejectCount,
     ResponseMaxSize,
     ResponseMaxSizeAcceptCount,
     ResponseMaxSizeRejectCount,
     ResponseMinSize,
     ResponseMinSizeAcceptCount,
     ResponseMinSizeRejectCount,
     ShowProgress,
     StatusFilterAcceptCount,
     StatusFilterRejectCount,
     UserAgentFilterAcceptCount,
     UserAgentFilterRejectCount,
     ViewRecords;

int  StatusCodeCount [6];

float  ByteCountTotal;

char  *AuthUserFilterPtr,
      *CgiFormLogFileSpecPtr,
      *CgiLibEnvironmentPtr,
      *CgiScriptNamePtr,
      *CgiServerNamePtr,
      *ClientFilterPtr,
      *DateTimeFilterPtr,
      *IpVersionFilterPtr,
      *LastModifiedBeforePtr,
      *LastModifiedSincePtr,
      *MethodFilterPtr,
      *OutputPtr,
      *PathFilterPtr,
      *ProtocolFilterPtr,
      *QueryFilterPtr,
      *RegCompPatternPtr,
      *RefererFilterPtr,
      *RemoteIdentFilterPtr,
      *SearchDna,
      *ResponseFilterPtr,
      *UserAgentFilterPtr;

char  LogFileSpec [256],
      RegCompErrorString [64],
      SoftwareID [64],
      StartDateTime [32];

$DESCRIPTOR (StartDateTimeFaoDsc, "!17%D\0");
$DESCRIPTOR (StartDateTimeDsc, StartDateTime);

#define INETACP$C_TRANS 2
#define INETACP_FUNC$C_GETHOSTBYNAME 1
#define INETACP_FUNC$C_GETHOSTBYADDR 2
$DESCRIPTOR (TcpIpDeviceDsc, "UCX$DEVICE");

/* required function prototypes */
void CgiStats ();
void CgiForm ();
void CliStats ();
BOOL CompileFilters ();
int DayMonthYear (char*);
char* DateTimeFilter (char*);
BOOL FilterThisOut (char*, char*);
int GetParameters ();
void NeedsPrivilegedAccount ();
void OutputLogRecord (char*);
int ProcessLogFile (char*);
int ProcessLogRecord (char*);
int SearchFileSpec (char*);
int ShowHelp ();
int strsame (char*, char*, int);
char* SysGetMsg (int);
char* SysTrnLnmLnmSystem (char*);
char* TcpIpHostCache (char*, int*, int*);
char* TcpIpLookupAddress (char*, int*, int*);
char* TcpIpLookupHostName (char*, int, int*);
int TcpIpStringAddress (char*, int*, int*);

/*****************************************************************************/
/*
'argc' and 'argv' are only required to support CgiLibEnvironmentInit(), in
particular the OSU environment.
*/

main
(
int argc,
char *argv[]
)
{
   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CGILIB_SOFTWAREID);

   /* always initialize the CGILIB so we can at least determine the mode */
   if (getenv ("QDLOGSTATS$DBUG")) Debug = true;
   CgiLibEnvironmentSetDebug (Debug);
   if (Debug) fprintf (stdout, "Content-Type: text/plain\n\n");

   GetParameters ();

   CgiLibEnvironmentInit (argc, argv, false);

   /* determine if we're at the command line or being used as a script */
   if (!CgiLibVarNull ("WWW_GATEWAY_INTERFACE"))
   {
      DoCliStats = true;
      CliStats ();
   }
   else
   {
      DoCgiStats = true;
      CgiStats ();
   }

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Generate command-line output containing the statistics.
*/

void CliStats ()

{
   static char  ModifiedBetween [] =
      "Modified ..... between %11.11s and %11.11s\n";

   int  status,
        DurationHr,
        DurationMin,
        DurationSec;
   char  *cptr;
   char  AccessProblemString [256],
         AuthUserString [256],
         ByteString [256],
         ClientString [256],
         DateTimeString [256],
         IgnoredString [256],
         LastModifiedString [128],
         IpVersionString [128],
         MethodString [256],
         PathString [256],
         ProtocolString [256],
         QueryString [256],
         RefererString [256],
         RemoteIdentString [256],
         ResponseMaxSizeString [128],
         ResponseMinSizeString [128],
         ResponseString [128],
         UserAgentString [256];
   unsigned long  StartSeconds,
                  TimeSeconds;

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

   NeedsPrivilegedAccount ();

   if (OutputPtr)
      if (!(stdout = freopen (OutputPtr, "w", stdout)))
         exit (vaxc$errno);

   if (!LogFileSpec[0])
   {
      ShowHelp ();
      exit (SS$_NORMAL);
   }

   if (LastModifiedSincePtr)
      if (!(CtimeSince = DayMonthYear (LastModifiedSincePtr)))
         exit (SS$_IVTIME);

   if (LastModifiedBeforePtr)
      if (!(CtimeBefore = DayMonthYear (LastModifiedBeforePtr)))
         exit (SS$_IVTIME);

   sys$fao (&StartDateTimeFaoDsc, NULL, &StartDateTimeDsc, 0);
   if (StartDateTime[0] == ' ') strcpy (StartDateTime, StartDateTime+1);
   fprintf (stdout, "%s, %s\n", SoftwareID, StartDateTime);
   time (&StartSeconds);

   if (LastModifiedSincePtr && LastModifiedBeforePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               LastModifiedSincePtr, LastModifiedBeforePtr);
   else
   if (LastModifiedSincePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               LastModifiedSincePtr, StartDateTime);
   else
   if (LastModifiedBeforePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               StartDateTime, LastModifiedBeforePtr);
   else
      LastModifiedString[0] = '\0';

   if (!CompileFilters ())
   {
      fprintf (stdout, "%%%s-E-REGEX, expression \'%s\' has %s",
               Utility, RegCompPatternPtr, RegCompErrorString);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }

   ProgressWrap = PROGRESS_WRAP_CLI;

   /* make a guess at the log file defaults */
   if (getenv("HT_ROOT"))
   {
      SearchDna = DEFAULT_LOGS_DIR_WASD;
      cptr = DEFAULT_LOGS_WASD;
   }
   else
   if (getenv("WWW_ROOT"))
   {
      SearchDna = DEFAULT_LOGS_DIR_OSU ;
      cptr = DEFAULT_LOGS_OSU;
   }
   else
   if (getenv("APACHE$SPECIFIC"))
   {
      SearchDna = DEFAULT_LOGS_DIR_APACHE;
      cptr = DEFAULT_LOGS_APACHE;
   }
   else
   {
      SearchDna = DEFAULT_LOGS_FILESPEC;
      cptr = DEFAULT_LOGS_FILESPEC;
   }

   if (LogFileSpec[0]) cptr = LogFileSpec;

   status = SearchFileSpec (cptr);

   if (ShowProgress && FileCountTotal) fputs ("\n", stdout);

   if (VMSnok (status) && status != RMS$_NMF) exit (status);

   if (AccessProblemCount)
      sprintf (AccessProblemString, ", %d access failed", AccessProblemCount);
   else
      AccessProblemString[0] = '\0';

   if (LastModifiedIgnoredCount)
      sprintf (IgnoredString, ", %d ignored", LastModifiedIgnoredCount);
   else
      IgnoredString[0] = '\0';

   if (ByteCountTotal > 1000000000.0)
   {
      ByteCountTotal /= 1000000000.0;
      sprintf (ByteString, "GBytes ....... %.3f", ByteCountTotal);
   }
   else
   if (ByteCountTotal > 1000000.0)
   {
      ByteCountTotal /= 1000000.0;
      sprintf (ByteString, "MBytes ........ %.3f", ByteCountTotal);
   }
   else
   if (ByteCountTotal > 1000.0)
   {
      ByteCountTotal /= 1000.0;
      sprintf (ByteString, "KBytes ........ %.3f", ByteCountTotal);
   }
   else
      sprintf (ByteString, "Bytes ......... %.0f", ByteCountTotal);

   time (&TimeSeconds);
   TimeSeconds -= StartSeconds;
   DurationHr = TimeSeconds / 3600;
   TimeSeconds -= DurationHr * 3600;
   DurationMin = TimeSeconds / 60;
   DurationSec = TimeSeconds - DurationMin * 60;

   if (!ResponseMinSize)
      strcpy (ResponseMinSizeString, "<none>");
   else
   if (ResponseMinSize < 1000)
      sprintf (ResponseMinSizeString, "%d", ResponseMinSize); 
   else
   if (ResponseMinSize < 1000000)
      sprintf (ResponseMinSizeString, "%dk", ResponseMinSize / 1000); 
   else
      sprintf (ResponseMinSizeString, "%dM", ResponseMinSize / 1000000); 

   if (!ResponseMaxSize)
      strcpy (ResponseMaxSizeString, "<none>");
   else
   if (ResponseMaxSize < 1000)
      sprintf (ResponseMaxSizeString, "%d", ResponseMaxSize); 
   else
   if (ResponseMaxSize < 1000000)
      sprintf (ResponseMaxSizeString, "%dk", ResponseMaxSize / 1000); 
   else
      sprintf (ResponseMaxSizeString, "%dM", ResponseMaxSize / 1000000); 

   if (FilterOnAll)
   {
      if (!ClientFilterPtr)
         strcpy (ClientString, "<none>");
      else
         sprintf (ClientString, "%s  (%d nomatch, %d match)",
                  ClientFilterPtr, ClientFilterRejectCount,
                                   ClientFilterAcceptCount);
      if (!IpVersionFilter)
         strcpy (IpVersionString, "<none>");
      else
      if (IpVersionFilter == 4)
         sprintf (IpVersionString, "4  (%d nomatch, %d match)",
                  Ip4FilterRejectCount, Ip4FilterAcceptCount);
      else
      if (IpVersionFilter == 6)
         sprintf (IpVersionString, "6  (%d nomatch, %d match)",
                  Ip6FilterRejectCount, Ip6FilterAcceptCount);
      if (!AuthUserFilterPtr)
         strcpy (AuthUserString, "<none>");
      else
         sprintf (AuthUserString, "%s  (%d nomatch, %d match)",
                  AuthUserFilterPtr, AuthUserFilterRejectCount,
                                     AuthUserFilterAcceptCount);
      if (!DateTimeFilterPtr)
         strcpy (DateTimeString, "<none>");
      else
         sprintf (DateTimeString, "%s  (%d nomatch, %d match)",
                  DateTimeFilterPtr, DateTimeFilterRejectCount,
                                     DateTimeFilterAcceptCount);
      if (!MethodFilterPtr)
         strcpy (MethodString, "<none>");
      else
         sprintf (MethodString, "%s  (%d nomatch, %d match)",
                  MethodFilterPtr, MethodFilterRejectCount,
                                   MethodFilterAcceptCount);
      if (!PathFilterPtr)
         strcpy (PathString, "<none>");
      else
         sprintf (PathString, "%s  (%d nomatch, %d match)",
                  PathFilterPtr, PathFilterRejectCount,
                                 PathFilterAcceptCount);
      if (!QueryFilterPtr)
         strcpy (QueryString, "<none>");
      else
         sprintf (QueryString, "  (%d nomatch, %d match)",
                  QueryFilterRejectCount, QueryFilterAcceptCount);
      if (!RefererFilterPtr)
         strcpy (RefererString, "<none>");
      else
         sprintf (RefererString, "%s  (%d nomatch, %d match)",
                  RefererFilterPtr, RefererFilterRejectCount,
                                    RefererFilterAcceptCount);
      if (!ProtocolFilterPtr)
         strcpy (ProtocolString, "<none>");
      else
         sprintf (ProtocolString, "  (%d nomatch, %d match)",
                  ProtocolFilterRejectCount, ProtocolFilterAcceptCount);
      if (!RefererFilterPtr)
         strcpy (RefererString, "<none>");
      else
         sprintf (RefererString, "%s  (%d nomatch, %d match)",
                  RefererFilterPtr, RefererFilterRejectCount,
                                    RefererFilterAcceptCount);
      if (!RemoteIdentFilterPtr)
         strcpy (RemoteIdentString, "<none>");
      else
         sprintf (RemoteIdentString, "%s  (%d nomatch, %d match)",
                  RemoteIdentFilterPtr, RemoteIdentFilterRejectCount,
                                        RemoteIdentFilterAcceptCount);
      if (!UserAgentFilterPtr)
         strcpy (UserAgentString, "<none>");
      else
         sprintf (UserAgentString, "%s  (%d nomatch, %d match)",
                  UserAgentFilterPtr, UserAgentFilterRejectCount,
                                      UserAgentFilterAcceptCount);
      if (!ResponseFilterPtr)
         strcpy (ResponseString, "<none>");
      else
         sprintf (ResponseString, "%s  (%d nomatch, %d match)",
                  ResponseFilterPtr, StatusFilterRejectCount,
                                     StatusFilterAcceptCount);
      if (ResponseMinSize)
      {
         for (cptr = ResponseMinSizeString; *cptr; cptr++);
         sprintf (cptr, "  (%d nomatch, %d match)",
                  ResponseMinSizeRejectCount, ResponseMinSizeAcceptCount);
      }
      if (ResponseMaxSize)
      {
         for (cptr = ResponseMaxSizeString; *cptr; cptr++);
         sprintf (cptr, "  (%d nomatch, %d match)",
                  ResponseMaxSizeRejectCount, ResponseMaxSizeAcceptCount);
      }
   }
   else
   {
      if (!ClientFilterPtr)
         strcpy (ClientString, "<none>");
      else
         sprintf (ClientString, "%s  (%d nomatch)",
                  ClientFilterPtr, ClientFilterRejectCount);
      if (!IpVersionFilter)
         strcpy (IpVersionString, "<none>");
      else
      if (IpVersionFilter == 4)
         sprintf (IpVersionString, "4  (%d nomatch)",
                  Ip4FilterRejectCount);
      else
      if (IpVersionFilter == 6)
         sprintf (IpVersionString, "6  (%d nomatch)",
                  Ip6FilterRejectCount);
      if (!AuthUserFilterPtr)
         strcpy (AuthUserString, "<none>");
      else
         sprintf (AuthUserString, "%s  (%d nomatch)",
                  AuthUserFilterPtr, AuthUserFilterRejectCount);
      if (!DateTimeFilterPtr)
         strcpy (DateTimeString, "<none>");
      else
         sprintf (DateTimeString, "%s  (%d nomatch)",
                  DateTimeFilterPtr, DateTimeFilterRejectCount);
      if (!MethodFilterPtr)
         strcpy (MethodString, "<none>");
      else
         sprintf (MethodString, "%s  (%d nomatch)",
                  MethodFilterPtr, MethodFilterRejectCount);
      if (!PathFilterPtr)
         strcpy (PathString, "<none>");
      else
         sprintf (PathString, "%s  (%d nomatch)",
                  PathFilterPtr, PathFilterRejectCount);
      if (!QueryFilterPtr)
         strcpy (QueryString, "<none>");
      else
         sprintf (QueryString, "%s  (%d nomatch)",
                  QueryFilterPtr, QueryFilterRejectCount);
      if (!ProtocolFilterPtr)
         strcpy (ProtocolString, "<none>");
      else
         sprintf (ProtocolString, "%s  (%d nomatch)",
                  ProtocolFilterPtr, ProtocolFilterRejectCount);
      if (!RefererFilterPtr)
         strcpy (RefererString, "<none>");
      else
         sprintf (RefererString, "%s  (%d nomatch)",
                  RefererFilterPtr, RefererFilterRejectCount);
      if (!RemoteIdentFilterPtr)
         strcpy (RemoteIdentString, "<none>");
      else
         sprintf (RemoteIdentString, "%s  (%d nomatch)",
                  RemoteIdentFilterPtr, RemoteIdentFilterRejectCount);
      if (!UserAgentFilterPtr)
         strcpy (UserAgentString, "<none>");
      else
         sprintf (UserAgentString, "%s  (%d nomatch)",
                  UserAgentFilterPtr, UserAgentFilterRejectCount);
      if (!ResponseFilterPtr)
         strcpy (ResponseString, "<none>");
      else
         sprintf (ResponseString, "%s  (%d nomatch)",
                  ResponseFilterPtr, StatusFilterRejectCount);
      if (ResponseMinSize)
      {
         for (cptr = ResponseMinSizeString; *cptr; cptr++);
         sprintf (cptr, "  (%d nomatch)",
                  ResponseMinSizeRejectCount);
      }
      if (ResponseMaxSize)
      {
         for (cptr = ResponseMaxSizeString; *cptr; cptr++);
         sprintf (cptr, "  (%d nomatch)",
                  ResponseMaxSizeRejectCount);
      }
   }

   fprintf (stdout,
"\n\
Log ........... %s  (%d matched%s%s)\n\
%s\
Client ........ %s\n\
IP Version .... %s\n\
Remote ID ..... %s\n\
Auth User ..... %s\n\
Date/Time ..... %s\n\
Method ........ %s\n\
Path .......... %s\n\
Query ......... %s\n\
Protocol ...... %s\n\
Referer ....... %s\n\
User Agent .... %s\n\
\n\
Response ...... %s\n\
Size Min ...... %s\n\
Size Max ...... %s\n\
\n\
Duration ...... %02d:%02d:%02d\n\
Records ....... %d  (%d suspect, %d common, %d combined)\n\
Requests ...... %d\n\
Methods ....... CONNECT:%d  DELETE:%d  GET:%d  HEAD:%d  POST:%d  PUT:%d  ?:%d\n\
Responses ..... 1nn:%d  2nn:%d  3nn:%d  4nn:%d  5nn:%d  ?:%d\n\
%s\n",
      LogFileSpec, FileCountTotal, IgnoredString, AccessProblemString,
      LastModifiedString,
      ClientString,
      IpVersionString,
      RemoteIdentString,
      AuthUserString,
      DateTimeString,
      MethodString,
      PathString,
      QueryString,
      ProtocolString,
      RefererString,
      UserAgentString,
      ResponseString,
      ResponseMinSizeString,
      ResponseMaxSizeString,
      DurationHr, DurationMin, DurationSec,
      RecordCountTotal, SuspectRecordCountTotal,
      CommonLogRecordCount, CombinedLogRecordCount,
      RequestCountTotal,
      MethodConnectCount,
      MethodDeleteCount,
      MethodGetCount,
      MethodHeadCount,
      MethodPostCount,
      MethodPutCount,
      MethodUnknownCount,
      StatusCodeCount[1],
      StatusCodeCount[2],
      StatusCodeCount[3],
      StatusCodeCount[4],
      StatusCodeCount[5],
      StatusCodeCount[0],
      ByteString);
}      

/*****************************************************************************/
/*
Generate a CGI response containing an HTML page of statistics.
*/

void CgiStats ()

{
#ifndef PRV$M_READALL
#define PRV$M_READALL 0x0008
#endif

   static unsigned long  PrvMask [2] =
      { (unsigned long)PRV$M_SYSPRV | (unsigned long)PRV$M_READALL, 0 };

   static char  ModifiedBetween [] =
"<TR><TH ALIGN=right>Modified:&nbsp;</TH>\
<TD>between %11.11s and %11.11s</TD></TR>\n";

   BOOL  ShowProgressOnPage;
   int  status,
        DurationHr,
        DurationMin,
        DurationSec;
   char  *cptr,
         *ByteStringPtr;
   char  AccessProblemString [256],
         AuthUserString [256],
         ByteString [16],
         ClientString [256],
         DateTimeString [256],
         IgnoredString [256],
         IpVersionString [128],
         LastModifiedString [128],
         MethodString [256],
         PathString [256],
         ProtocolString [256],
         QueryString [256],
         RefererString [256],
         RemoteIdentString [256],
         ResponseMaxSizeString [128],
         ResponseMinSizeString [128],
         ResponseString [128],
         UserAgentString [256],
         VmsMessage [512];
   unsigned long  StartSeconds,
                  TimeSeconds;

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

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   /* ensure one of the required log directory logicals is defined */
   if (!(SearchDna = SysTrnLnmLnmSystem(DEFAULT_LOGS_LOGICAL)))
      if (!(SearchDna = SysTrnLnmLnmSystem(DEFAULT_LOGS2_LOGICAL)))
         if (!(SearchDna = SysTrnLnmLnmSystem(DEFAULT_LOGS3_LOGICAL)))
         {
            CgiLibResponseError (FI_LI, 0, "Logical name "
                                           DEFAULT_LOGS_LOGICAL
                                           " is not defined.");
            return;
         }
   
   sys$fao (&StartDateTimeFaoDsc, NULL, &StartDateTimeDsc, 0);
   if (StartDateTime[0] == ' ') strcpy (StartDateTime, StartDateTime+1);
   time (&StartSeconds);

   cptr = CgiLibVar ("WWW_REMOTE_USER");
   if (!cptr[0])
   {
      CgiLibResponseError (FI_LI, 0, "Authorization mandatory.");
      return;
   }

   CgiLibEnvironmentRecordOut ();
   CgiLibEnvironmentPtr = CgiLibEnvironmentName();
   CgiServerNamePtr = CgiLibVar ("WWW_SERVER_NAME");

   CgiFormLogFileSpecPtr = CgiLibVarNull ("WWW_FORM_LOGFILESPEC");
   if (!CgiFormLogFileSpecPtr)
   {
      CgiForm ();
      return;
   }

   LastModifiedSincePtr = CgiLibVarNull ("WWW_FORM_LOGMODSINCE");
   if (LastModifiedSincePtr && *(long*)LastModifiedSincePtr == 'dd-m')
      LastModifiedSincePtr = NULL;
   if (LastModifiedSincePtr)
      if (!(CtimeSince = DayMonthYear (LastModifiedSincePtr)))
      {
         CgiLibResponseError (FI_LI, SS$_IVTIME, "modified since.");
         exit (SS$_NORMAL);
      }

   LastModifiedBeforePtr = CgiLibVarNull ("WWW_FORM_LOGMODBEFORE");
   if (LastModifiedBeforePtr && *(long*)LastModifiedBeforePtr == 'dd-m')
      LastModifiedBeforePtr = NULL;
   if (LastModifiedBeforePtr)
      if (!(CtimeBefore = DayMonthYear (LastModifiedBeforePtr)))
      {
         CgiLibResponseError (FI_LI, SS$_IVTIME, "modified before.");
         exit (SS$_NORMAL);
      }

   if (LastModifiedSincePtr && LastModifiedBeforePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               LastModifiedSincePtr, LastModifiedBeforePtr);
   else
   if (LastModifiedSincePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               LastModifiedSincePtr, StartDateTime);
   else
   if (LastModifiedBeforePtr)
      sprintf (LastModifiedString, ModifiedBetween,
               StartDateTime, LastModifiedBeforePtr);
   else
      LastModifiedString[0] = '\0';

   /* get the filter components from the form */
   ClientFilterPtr = CgiLibVar ("WWW_FORM_CLIENT");
   if (!ClientFilterPtr[0]) ClientFilterPtr = NULL;
   IpVersionFilterPtr = CgiLibVar ("WWW_FORM_IPV");
   if (*IpVersionFilterPtr == '4')
      IpVersionFilter = 4;
   else
   if (*IpVersionFilterPtr == '6')
      IpVersionFilter = 6;
   else
      IpVersionFilter = 0;
   RemoteIdentFilterPtr = CgiLibVar ("WWW_FORM_REMOTEID");
   if (!RemoteIdentFilterPtr[0]) RemoteIdentFilterPtr = NULL;
   AuthUserFilterPtr = CgiLibVar ("WWW_FORM_AUTHUSER");
   if (!AuthUserFilterPtr[0])  AuthUserFilterPtr = NULL;
   DateTimeFilterPtr = CgiLibVarNull ("WWW_FORM_DATETIME");
   if (DateTimeFilterPtr)
      DateTimeFilterPtr = DateTimeFilter (DateTimeFilterPtr);
   MethodFilterPtr = CgiLibVar ("WWW_FORM_METHOD");
   if (!MethodFilterPtr[0])
      MethodFilterPtr = CgiLibVar ("WWW_FORM_METHODLIST");
   if (!MethodFilterPtr[0]) MethodFilterPtr = NULL;
   PathFilterPtr = CgiLibVar ("WWW_FORM_PATH");
   if (!PathFilterPtr[0]) PathFilterPtr = NULL;
   cptr = CgiLibVar ("WWW_FORM_DECODEPATH");
   if (cptr[0]) UrlDecodePath = true; else UrlDecodePath = false;
   ProtocolFilterPtr = CgiLibVar ("WWW_FORM_PROTOCOL");
   if (!ProtocolFilterPtr[0])
      ProtocolFilterPtr = CgiLibVar ("WWW_FORM_PROTOCOLLIST");
   if (!ProtocolFilterPtr[0]) ProtocolFilterPtr = NULL;
   QueryFilterPtr = CgiLibVar ("WWW_FORM_QUERY");
   if (!QueryFilterPtr[0]) QueryFilterPtr = NULL;
   cptr = CgiLibVar ("WWW_FORM_DECODEQUERY");
   if (cptr[0]) UrlDecodeQuery = true; else UrlDecodeQuery = false;
   RefererFilterPtr = CgiLibVar ("WWW_FORM_REFERER");
   if (!RefererFilterPtr[0]) RefererFilterPtr = NULL;
   cptr = CgiLibVar ("WWW_FORM_DECODEREFERER");
   if (cptr[0]) UrlDecodeReferer = true; else UrlDecodeReferer = false;
   UserAgentFilterPtr = CgiLibVar ("WWW_FORM_USERAGENT");
   if (!UserAgentFilterPtr[0]) UserAgentFilterPtr = NULL;
   ResponseFilterPtr = CgiLibVar ("WWW_FORM_RESPONSE");
   if (!ResponseFilterPtr[0])
      ResponseFilterPtr = CgiLibVar ("WWW_FORM_RESPONSELIST");
   if (!ResponseFilterPtr[0]) ResponseFilterPtr = NULL;

   cptr = CgiLibVar ("WWW_FORM_VIEW");
   ShowLogFile = ShowProgressOnPage = false;
   if (strsame (cptr, "ALL", -1))
      ViewRecords = VIEW_ALL;
   else
   if (strsame (cptr, "ALL-LOG", -1))
   {
      ViewRecords = VIEW_ALL;
      ShowLogFile = true;
   }
   else
   if (strsame (cptr, "LOG", -1))
   {
      ViewRecords = VIEW_LOG;
      ShowLogFile = true;
   }
   else
   if (strsame (cptr, "MATCH", -1))
      ViewRecords = VIEW_MATCH;
   else
   if (strsame (cptr, "MATCH-LOG", -1))
   {
      ViewRecords = VIEW_MATCH;
      ShowLogFile = true;
   }
   else
   if (strsame (cptr, "NOMATCH", -1))
      ViewRecords = VIEW_NOMATCH;
   else
   if (strsame (cptr, "NOMATCH-LOG", -1))
   {
      ViewRecords = VIEW_NOMATCH;
      ShowLogFile = true;
   }
   else
   if (strsame (cptr, "PROGRESS", -1))
      ShowProgressOnPage = true;

   cptr = CgiLibVar ("WWW_FORM_ALL");
   if (cptr[0]) FilterOnAll = true;

   cptr = CgiLibVar ("WWW_FORM_LOOKUP");
   if (isdigit(cptr[0])) LookupRetry = atoi(cptr);

   cptr = CgiLibVar ("WWW_FORM_MINSIZE");
   if (!cptr[0]) cptr = CgiLibVar ("WWW_FORM_MINSIZELIST");
   ResponseMinSize = atoi(cptr);
   if (isdigit(*cptr))
   {
      while (isdigit(*cptr)) cptr++;
      if (tolower(*cptr) == 'k') ResponseMinSize *= 1000;
      if (toupper(*cptr) == 'M') ResponseMinSize *= 1000000 ;
   }
   if (!ResponseMinSize)
      strcpy (ResponseMinSizeString, "<I>&lt;none&gt;</I>");
   else
   if (ResponseMinSize < 1000)
      sprintf (ResponseMinSizeString, "%d", ResponseMinSize); 
   else
   if (ResponseMinSize < 1000000)
      sprintf (ResponseMinSizeString, "%dk", ResponseMinSize / 1000); 
   else
      sprintf (ResponseMinSizeString, "%dM", ResponseMinSize / 1000000); 

   cptr = CgiLibVar ("WWW_FORM_MAXSIZE");
   if (!cptr[0]) cptr = CgiLibVar ("WWW_FORM_MAXSIZELIST");
   ResponseMaxSize = atoi(cptr);
   if (isdigit(*cptr))
   {
      while (isdigit(*cptr)) cptr++;
      if (tolower(*cptr) == 'k') ResponseMaxSize *= 1000;
      if (toupper(*cptr) == 'M') ResponseMaxSize *= 1000000;
   }
   if (!ResponseMaxSize)
      strcpy (ResponseMaxSizeString, "<I>&lt;none&gt;</I>");
   else
   if (ResponseMaxSize < 1000)
      sprintf (ResponseMaxSizeString, "%d", ResponseMaxSize); 
   else
   if (ResponseMaxSize < 1000000)
      sprintf (ResponseMaxSizeString, "%dk", ResponseMaxSize / 1000); 
   else
      sprintf (ResponseMaxSizeString, "%dM", ResponseMaxSize / 1000000); 

   if (!CompileFilters ())
   {
      CgiLibResponseError (FI_LI, 0,
"The regular expression&nbsp; <TT>%s</TT>&nbsp; has %s.",
         (char*)CgiLibHtmlEscape(RegCompPatternPtr, -1, NULL, 0),
         RegCompErrorString);
      return;
   }

   /* WASD stream mode will stop each fflush() having carriage control added */
   CgiLibEnvironmentBinaryOut ();
   CgiLibResponseHeader (200, "text/html", "Script-Control: X-stream-mode\n");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<META NAME=\"dna\" CONTENT=\"%s\">\n\
<TITLE>qdLogStats@%s</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<FONT SIZE=+1><B><U>qdLogStats@%s</U></B></FONT><BR>\n\
<FONT SIZE=-3>&nbsp;</FONT>%s<BR>\n\
<FONT SIZE=-3>&nbsp;</FONT>%s\n",
      SoftwareID, CgiLibEnvironmentPtr, SearchDna,
      CgiServerNamePtr, CgiServerNamePtr,
      SOFTWAREID, StartDateTime);

   if (ViewRecords)
      fputs ("<P><HR SIZE=1 WIDTH=85% ALIGN=left NOSHADE>\n<PRE>", stdout);
   else
   {
      /* the CGI interface always provides hidden progress indication */
      ShowProgress = PROGRESS_RECORDS;
      if (ShowProgressOnPage)
         fputs ("<PRE>", stdout);
      else
         fputs ("<!-- just an indicator of progress ...\n", stdout);
   }
   fflush (stdout);

   ProgressWrap = PROGRESS_WRAP_CGI;

   /* just get the file portion of any file spec supplied */
   for (cptr = CgiFormLogFileSpecPtr; *cptr; cptr++);
   while (cptr > CgiFormLogFileSpecPtr && *cptr != ']') cptr--;
   if (*cptr == ']') cptr++;
   if (*cptr) CgiFormLogFileSpecPtr = cptr; 

   status = sys$setprv (1, &PrvMask, 0, 0);
   if (Debug) fprintf (stdout, "sys$setprv() %%X%08.08X\n", status);

   status = SearchFileSpec (CgiFormLogFileSpecPtr);

   sys$setprv (0, &PrvMask, 0, 0);

   if (ViewRecords)
      fputs ("</PRE>\n<HR SIZE=1 WIDTH=85% ALIGN=left NOSHADE>\n", stdout);
   else
   if (ShowProgressOnPage)
      fputs ("</PRE>\n", stdout);
   else
      fputs (" -->\n", stdout);

   if (VMSnok (status) && status != RMS$_NMF && status != RMS$_FNF)
      sprintf (VmsMessage,
"<TR><TH ALIGN=right><FONT COLOR=\"#ff0000\">ERROR</FONT>:&nbsp;</TH>\
<TD>%s<!-- %%X%08.08X --></TD></TR>\n\
<TR><TH HEIGHT=8></TH></TR>\n",
               SysGetMsg(status), status);
   else
      VmsMessage[0] = '\0';

   if (AccessProblemCount)
      sprintf (AccessProblemString, ", %d access failed", AccessProblemCount);
   else
      AccessProblemString[0] = '\0';

   if (LastModifiedIgnoredCount)
      sprintf (IgnoredString, ", %d ignored", LastModifiedIgnoredCount);
   else
      IgnoredString[0] = '\0';

   time (&TimeSeconds);
   TimeSeconds -= StartSeconds;
   DurationHr = TimeSeconds / 3600;
   TimeSeconds -= DurationHr * 3600;
   DurationMin = TimeSeconds / 60;
   DurationSec = TimeSeconds - DurationMin * 60;

   if (ByteCountTotal > 1000000000.0)
   {
      ByteCountTotal /= 1000000000.0;
      ByteStringPtr = "GBytes";
      sprintf (ByteString, "%.3f", ByteCountTotal);
   }
   else
   if (ByteCountTotal > 1000000.0)
   {
      ByteCountTotal /= 1000000.0;
      ByteStringPtr = "MBytes";
      sprintf (ByteString, "%.3f", ByteCountTotal);
   }
   else
   if (ByteCountTotal > 1000.0)
   {
      ByteCountTotal /= 1000.0;
      ByteStringPtr = "KBytes";
      sprintf (ByteString, "%.3f", ByteCountTotal);
   }
   else
   {
      ByteStringPtr = "Bytes";
      sprintf (ByteString, "%.0f", ByteCountTotal);
   }

   if (FilterOnAll)
   {
      if (!ClientFilterPtr)
         strcpy (ClientString, "<I>&lt;none&gt;</I>");
      else
         sprintf (ClientString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (ClientFilterPtr, -1, NULL, 0),
                  ClientFilterRejectCount, ClientFilterAcceptCount);
      if (!IpVersionFilter)
         strcpy (IpVersionString, "<I>&lt;none&gt;</I>");
      else
      if (IpVersionFilter == 4)
         sprintf (IpVersionString, "4  (%d nomatch, %d match)",
                  Ip4FilterRejectCount, Ip4FilterAcceptCount);
      else
      if (IpVersionFilter == 6)
         sprintf (IpVersionString, "6  (%d nomatch, %d match)",
                  Ip6FilterRejectCount, Ip6FilterAcceptCount);
      if (!AuthUserFilterPtr)
         strcpy (AuthUserString, "<I>&lt;none&gt;</I>");
      else
         sprintf (AuthUserString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (AuthUserFilterPtr, -1, NULL, 0),
                  AuthUserFilterRejectCount, AuthUserFilterAcceptCount);
      if (!DateTimeFilterPtr)
         strcpy (DateTimeString, "<I>&lt;none&gt;</I>");
      else
         sprintf (DateTimeString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (DateTimeFilterPtr, -1, NULL, 0),
                  DateTimeFilterRejectCount, DateTimeFilterAcceptCount);
      if (!MethodFilterPtr)
         strcpy (MethodString, "<I>&lt;none&gt;</I>");
      else
         sprintf (MethodString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (MethodFilterPtr, -1, NULL, 0),
                  MethodFilterRejectCount, MethodFilterAcceptCount);
      if (!PathFilterPtr)
         strcpy (PathString, "<I>&lt;none&gt;</I>");
      else
         sprintf (PathString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (PathFilterPtr, -1, NULL, 0),
                  PathFilterRejectCount, PathFilterAcceptCount);
      if (!QueryFilterPtr)
         strcpy (QueryString, "<I>&lt;none&gt;</I>");
      else
         sprintf (QueryString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (QueryFilterPtr, -1, NULL, 0),
                  QueryFilterRejectCount, QueryFilterAcceptCount);
      if (!ProtocolFilterPtr)
         strcpy (ProtocolString, "<I>&lt;none&gt;</I>");
      else
         sprintf (ProtocolString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (ProtocolFilterPtr, -1, NULL, 0),
                  ProtocolFilterRejectCount, ProtocolFilterAcceptCount);
      if (!RefererFilterPtr)
         strcpy (RefererString, "<I>&lt;none&gt;</I>");
      else
         sprintf (RefererString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (RefererFilterPtr, -1, NULL, 0),
                  RefererFilterRejectCount, RefererFilterAcceptCount);
      if (!RemoteIdentFilterPtr)
         strcpy (RemoteIdentString, "<I>&lt;none&gt;</I>");
      else
         sprintf (RemoteIdentString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (RemoteIdentFilterPtr, -1, NULL, 0),
                  RemoteIdentFilterRejectCount, RemoteIdentFilterAcceptCount);
      if (!UserAgentFilterPtr)
         strcpy (UserAgentString, "<I>&lt;none&gt;</I>");
      else
         sprintf (UserAgentString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (UserAgentFilterPtr, -1, NULL, 0),
                  UserAgentFilterRejectCount, UserAgentFilterAcceptCount);
      if (!ResponseFilterPtr)
         strcpy (ResponseString, "<I>&lt;all&gt;</I>");
      else
         sprintf (ResponseString, "&quot;%s&quot;&nbsp; (%d nomatch, %d match)",
                  (char*)CgiLibHtmlEscape (ResponseFilterPtr, -1, NULL, 0),
                  StatusFilterRejectCount, StatusFilterAcceptCount);
      if (ResponseMinSize)
      {
         for (cptr = ResponseMinSizeString; *cptr; cptr++);
         sprintf (cptr, "&nbsp; (%d nomatch, %d match)",
                  ResponseMinSizeRejectCount, ResponseMinSizeAcceptCount);
      }
      if (ResponseMaxSize)
      {
         for (cptr = ResponseMaxSizeString; *cptr; cptr++);
         sprintf (cptr, "&nbsp; (%d nomatch, %d match)",
                  ResponseMaxSizeRejectCount, ResponseMaxSizeAcceptCount);
      }
   }
   else
   {
      if (!ClientFilterPtr)
         strcpy (ClientString, "<I>&lt;none&gt;</I>");
      else
         sprintf (ClientString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (ClientFilterPtr, -1, NULL, 0),
                  ClientFilterRejectCount);
      if (!IpVersionFilter)
         strcpy (IpVersionString, "<I>&lt;none&gt;</I>");
      else
      if (IpVersionFilter == 4)
         sprintf (IpVersionString, "4  (%d nomatch)",
                  Ip4FilterRejectCount);
      else
      if (IpVersionFilter == 6)
         sprintf (IpVersionString, "6  (%d nomatch)",
                  Ip6FilterRejectCount);
      if (!AuthUserFilterPtr)
         strcpy (AuthUserString, "<I>&lt;none&gt;</I>");
      else
         sprintf (AuthUserString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (AuthUserFilterPtr, -1, NULL, 0),
                  AuthUserFilterRejectCount);
      if (!DateTimeFilterPtr)
         strcpy (DateTimeString, "<I>&lt;none&gt;</I>");
      else
         sprintf (DateTimeString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (DateTimeFilterPtr, -1, NULL, 0),
                  DateTimeFilterRejectCount);
      if (!MethodFilterPtr)
         strcpy (MethodString, "<I>&lt;none&gt;</I>");
      else
         sprintf (MethodString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (MethodFilterPtr, -1, NULL, 0),
                  MethodFilterRejectCount);
      if (!PathFilterPtr)
         strcpy (PathString, "<I>&lt;none&gt;</I>");
      else
         sprintf (PathString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (PathFilterPtr, -1, NULL, 0),
                  PathFilterRejectCount);
      if (!QueryFilterPtr)
         strcpy (QueryString, "<I>&lt;none&gt;</I>");
      else
         sprintf (QueryString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (QueryFilterPtr, -1, NULL, 0),
                  QueryFilterRejectCount);
      if (!ProtocolFilterPtr)
         strcpy (ProtocolString, "<I>&lt;none&gt;</I>");
      else
         sprintf (ProtocolString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (ProtocolFilterPtr, -1, NULL, 0),
                  ProtocolFilterRejectCount);
      if (!RefererFilterPtr)
         strcpy (RefererString, "<I>&lt;none&gt;</I>");
      else
         sprintf (RefererString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (RefererFilterPtr, -1, NULL, 0),
                  RefererFilterRejectCount);
      if (!RemoteIdentFilterPtr)
         strcpy (RemoteIdentString, "<I>&lt;none&gt;</I>");
      else
         sprintf (RemoteIdentString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (RemoteIdentFilterPtr, -1, NULL, 0),
                  RemoteIdentFilterRejectCount);
      if (!UserAgentFilterPtr)
         strcpy (UserAgentString, "<I>&lt;none&gt;</I>");
      else
         sprintf (UserAgentString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (UserAgentFilterPtr, -1, NULL, 0),
                  UserAgentFilterRejectCount);

      if (!ResponseFilterPtr)
         strcpy (ResponseString, "<I>&lt;none&gt;</I>");
      else
         sprintf (ResponseString, "&quot;%s&quot;&nbsp; (%d nomatch)",
                  (char*)CgiLibHtmlEscape (ResponseFilterPtr, -1, NULL, 0),
                  StatusFilterRejectCount);
      if (ResponseMinSize)
      {
         for (cptr = ResponseMinSizeString; *cptr; cptr++);
         sprintf (cptr, "&nbsp; (%d nomatch)",
                  ResponseMinSizeRejectCount);
      }
      if (ResponseMaxSize)
      {
         for (cptr = ResponseMaxSizeString; *cptr; cptr++);
         sprintf (cptr, "&nbsp; (%d nomatch)",
                  ResponseMaxSizeRejectCount);
      }
   }

   fprintf (stdout,
"<P><TABLE CELLPADDING=0 CELLSPACING=3 BORDER=0>\n\
%s\
<TR><TH ALIGN=right>Log:&nbsp;</TH><TD>%s%s(%d matched%s%s)</TD></TR>\n\
%s\
<TR><TH HEIGHT=8></TH></TR>\n\
<TR><TH ALIGN=right>Client:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>IP&nbsp;Version:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Remote ID:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Auth User:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Date/Time:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Method:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Path:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Query:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Protocol:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Referer:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>User Agent:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH HEIGHT=8></TH></TR>\n\
<TR><TH ALIGN=right>Response:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Size Min:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH ALIGN=right>Max:&nbsp;</TH><TD>%s</TD></TR>\n\
<TR><TH HEIGHT=8></TH></TR>\n\
<TR><TH ALIGN=right>Duration:&nbsp;</TH><TD>%02d:%02d:%02d</TD></TR>\n\
<TR><TH ALIGN=right>Records:&nbsp;</TH>\
<TD><NOBR>%d&nbsp; (%d suspect, %d common, %d combined)</NOBR></TD></TR>\n\
<TR><TH ALIGN=right>Requests:&nbsp;</TH><TD>%d</TD></TR>\n\
<TR><TH ALIGN=right>Methods:&nbsp;</TH>\
<TD><NOBR>CONNECT:%d&nbsp; DELETE:%d&nbsp; GET:%d&nbsp; HEAD:%d&nbsp; \
POST:%d&nbsp; PUT:%d&nbsp; ?:%d&nbsp;</NOBR></TD></TR>\n\
<TR><TH ALIGN=right>Responses:&nbsp;</TH>\
<TD><NOBR>1nn:%d&nbsp; 2nn:%d&nbsp; 3nn:%d&nbsp; 4nn:%d&nbsp; 5nn:%d&nbsp; \
?:%d&nbsp;</NOBR></TD></TR>\n\
<TR><TH ALIGN=right>%s:&nbsp;</TH><TD>%s</TD></TR>\n\
</TABLE>\n\
</BODY>\n\
</HTML>\n",
      VmsMessage,
      CgiFormLogFileSpecPtr,
      CgiFormLogFileSpecPtr[0] ? "&nbsp;&nbsp;" : "", FileCountTotal,
      IgnoredString,
      AccessProblemString,
      LastModifiedString,
      ClientString,
      IpVersionString,
      RemoteIdentString,
      AuthUserString,
      DateTimeString,
      MethodString,
      PathString,
      QueryString,
      ProtocolString,
      RefererString,
      UserAgentString,
      ResponseString,
      ResponseMinSizeString,
      ResponseMaxSizeString,
      DurationHr, DurationMin, DurationSec,
      RecordCountTotal, SuspectRecordCountTotal,
      CommonLogRecordCount, CombinedLogRecordCount,
      RequestCountTotal,
      MethodConnectCount,
      MethodDeleteCount,
      MethodGetCount,
      MethodHeadCount,
      MethodPostCount,
      MethodPutCount,
      MethodUnknownCount,
      StatusCodeCount[1],
      StatusCodeCount[2],
      StatusCodeCount[3],
      StatusCodeCount[4],
      StatusCodeCount[5],
      StatusCodeCount[0],
      ByteStringPtr, ByteString);
}

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

void CgiForm ()

{
   char  *cptr;

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

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

   /* make a guess at the log file defaults */
   if (CgiLibEnvironmentIsWasd())
      cptr = DEFAULT_LOGS_WASD;
   else
   if (CgiLibEnvironmentIsOsu())
      cptr = DEFAULT_LOGS_OSU;
   else
   if (CgiLibEnvironmentIsApache())
      cptr = DEFAULT_LOGS_APACHE;
   else
      cptr = DEFAULT_LOGS_FILESPEC;

   CgiLibEnvironmentPtr = CgiLibEnvironmentName();

   CgiScriptNamePtr = CgiLibVar ("WWW_SCRIPT_NAME");

   sys$fao (&StartDateTimeFaoDsc, NULL, &StartDateTimeDsc, 0);
   if (StartDateTime[0] == ' ') strcpy (StartDateTime, StartDateTime+1);

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<META NAME=\"environment\" CONTENT=\"%s\">\n\
<META NAME=\"dna\" CONTENT=\"%s\">\n\
<TITLE>qdLogStats@%s</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<FONT SIZE=+1><B><U>qdLogStats@%s</U></B></FONT><BR>\n\
<FONT SIZE=-3>&nbsp;</FONT>%s<BR>\n\
<FONT SIZE=-3>&nbsp;</FONT>%s\n\
<FORM METHOD=GET ACTION=\"%s\">\n\
<P><TABLE CELLPADDING=0 CELLSPACING=3 BORDER=0>\n\
<TR><TH ALIGN=right>Log&nbsp;Specification:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=logfilespec SIZE=30 VALUE=\"%s\">\
</TD></TR>\n\
<TR><TH ALIGN=right>Modified Since:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=logmodsince SIZE=11 VALUE=\"dd-mmm-%4.4s\">\
</TD></TR>\n\
<TR><TH ALIGN=right>Before:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=logmodbefore SIZE=11 VALUE=\"dd-mmm-%4.4s\">\
</TD></TR>\n\
<TR><TD HEIGHT=7></TD></TR>\n\
<TR><TH ALIGN=right>Client:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=client SIZE=30>\n\
&nbsp;<INPUT TYPE=checkbox NAME=lookup VALUE=2>\
&nbsp;<FONT SIZE=-1><I>lookup&nbsp;name</I></FONT>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>IP&nbsp;Version:&nbsp;</TH><TD>\n\
<SELECT NAME=ipv>\n\
<OPTION VALUE=0 SELECTED>all\n\
<OPTION VALUE=4>4 only\n\
<OPTION VALUE=6>6 only\n\
</SELECT>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Remote&nbsp;ID:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=remoteid SIZE=30></TD></TR>\n\
<TR><TH ALIGN=right>Auth User:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=authuser SIZE=30></TD></TR>\n\
<TR><TH ALIGN=right>Date/Time:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=datetime SIZE=30 \
VALUE=\"dd/mmm/yyyy:hh:mm:ss &#177;hhmm\"></TD></TR>\n\
<TR><TH ALIGN=right>Method:&nbsp;</TH><TD>\n\
<SELECT NAME=methodlist>\n\
<OPTION VALUE=\"\" SELECTED>all\n\
<OPTION VALUE=CONNECT>CONNECT\n\
<OPTION VALUE=DELETE>DELETE\n\
<OPTION VALUE=GET>GET\n\
<OPTION VALUE=HEAD>HEAD\n\
<OPTION VALUE=POST>POST\n\
<OPTION VALUE=PUT>PUT\n\
</SELECT>\
&nbsp;<FONT SIZE=-1><I>or</I></FONT>&nbsp;\
<INPUT TYPE=text NAME=method SIZE=10>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Path:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=path SIZE=30>\n\
&nbsp;<INPUT TYPE=checkbox NAME=decodePath VALUE=1>\
&nbsp;<FONT SIZE=-1><I>decode&nbsp;first</I></FONT>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Query:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=query SIZE=30>\n\
&nbsp;<INPUT TYPE=checkbox NAME=decodeQuery VALUE=1>\
&nbsp;<FONT SIZE=-1><I>decode&nbsp;first</I></FONT>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Protocol:&nbsp;</TH><TD>\n\
<SELECT NAME=protocollist>\n\
<OPTION VALUE=\"\" SELECTED>all\n\
<OPTION VALUE=\"^^$\">HTTP/0.9\n\
<OPTION VALUE=\"HTTP/1.0\">HTTP/1.0\n\
<OPTION VALUE=\"HTTP/1.1\">HTTP/1.1\n\
</SELECT>\
&nbsp;<FONT SIZE=-1><I>or</I></FONT>&nbsp;\
<INPUT TYPE=text NAME=protocol SIZE=10>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Referer:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=referer SIZE=30>\n\
&nbsp;<INPUT TYPE=checkbox NAME=decodeReferer VALUE=1>\
&nbsp;<FONT SIZE=-1><I>decode&nbsp;first</I></FONT>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>User Agent:&nbsp;</TH>\n\
<TD><INPUT TYPE=text NAME=useragent SIZE=30></TD></TR>\n\
<TR><TD HEIGHT=8></TD></TR>\n\
<TR><TH ALIGN=right>Response:&nbsp;</TH><TD>\n\
<SELECT NAME=responselist>\n\
<OPTION VALUE=\"\" SELECTED>all\n\
<OPTION VALUE=2**>2nn - general success\n\
<OPTION VALUE=200>200 - success\n\
<OPTION VALUE=201>201 - created\n\
<OPTION VALUE=202>202 - accepted\n\
<OPTION VALUE=206>206 - partial content\n\
<OPTION VALUE=3**>3nn - client action\n\
<OPTION VALUE=300>300 - multiple choices\n\
<OPTION VALUE=301>301 - moved perm\n\
<OPTION VALUE=302>302 - moved temp\n\
<OPTION VALUE=304>304 - not modified\n\
<OPTION VALUE=4**>4nn - client error\n\
<OPTION VALUE=400>400 - bad request\n\
<OPTION VALUE=401>401 - authorization\n\
<OPTION VALUE=403>403 - forbidden\n\
<OPTION VALUE=404>404 - not found\n\
<OPTION VALUE=407>407 - proxy auth\n\
<OPTION VALUE=409>409 - conflict\n\
<OPTION VALUE=5**>5nn - server error\n\
<OPTION VALUE=500>500 - internal\n\
<OPTION VALUE=501>501 - not implemented\n\
<OPTION VALUE=502>502 - bad gateway\n\
<OPTION VALUE=503>503 - unavailable\n\
<OPTION VALUE=000>error (no status)\n\
</SELECT>\
&nbsp;<FONT SIZE=-1><I>or</I></FONT>&nbsp;\
<INPUT TYPE=text NAME=response SIZE=3>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Size Min:&nbsp;</TH><TD>\n\
<SELECT NAME=minsizelist>\n\
<OPTION VALUE=\"\" SELECTED>none\n\
<OPTION VALUE=100>100\n\
<OPTION VALUE=250>250\n\
<OPTION VALUE=500>500\n\
<OPTION VALUE=1k>1k\n\
<OPTION VALUE=2.5k>2.5k\n\
<OPTION VALUE=5k>5k\n\
<OPTION VALUE=10k>10k\n\
<OPTION VALUE=25k>25k\n\
<OPTION VALUE=50k>50k\n\
<OPTION VALUE=100k>100k\n\
<OPTION VALUE=250k>250k\n\
<OPTION VALUE=500k>500k\n\
<OPTION VALUE=1M>1M\n\
<OPTION VALUE=10M>10M\n\
<OPTION VALUE=100M>100M\n\
</SELECT>\
&nbsp;<FONT SIZE=-1><I>or</I></FONT>&nbsp;\
<INPUT TYPE=text NAME=minsize SIZE=6>\n\
</TD></TR>\n\
<TR><TH ALIGN=right>Max:&nbsp;</TH><TD>\n\
<SELECT NAME=maxsizelist>\n\
<OPTION VALUE=\"\" SELECTED>none\n\
<OPTION VALUE=100>100\n\
<OPTION VALUE=250>250\n\
<OPTION VALUE=500>500\n\
<OPTION VALUE=1k>1k\n\
<OPTION VALUE=2.5k>2.5k\n\
<OPTION VALUE=5k>5k\n\
<OPTION VALUE=10k>10k\n\
<OPTION VALUE=25k>25k\n\
<OPTION VALUE=50k>50k\n\
<OPTION VALUE=100k>100k\n\
<OPTION VALUE=250k>250k\n\
<OPTION VALUE=500k>500k\n\
<OPTION VALUE=1M>1M\n\
<OPTION VALUE=10M>10M\n\
<OPTION VALUE=100M>100M\n\
</SELECT>\
&nbsp;<FONT SIZE=-1><I>or</I></FONT>&nbsp;\
<INPUT TYPE=text NAME=maxsize SIZE=6>\n\
</TD></TR>\n\
<TR><TD HEIGHT=8></TD></TR>\n\
<TR><TH ALIGN=right>View:&nbsp;</TH><TD>\n\
<SELECT NAME=view>\n\
<OPTION VALUE=none SELECTED>none\n\
<OPTION VALUE=progress>progress\n\
<OPTION VALUE=match>matching records\n\
<OPTION VALUE=match-log>matching + log names\n\
<OPTION VALUE=nomatch>non-matching records\n\
<OPTION VALUE=nomatch-log>non-matching + log names\n\
<OPTION VALUE=all>all records\n\
<OPTION VALUE=all-log>all + log names\n\
<OPTION VALUE=log>log names\n\
</SELECT>\n\
&nbsp;<INPUT TYPE=checkbox NAME=all VALUE=1>\
<FONT SIZE=-1>&nbsp;<I>match all</I></FONT>\n\
</TD></TR>\n\
<TR><TH HEIGHT=4></TH></TR>\n\
<TR><TH></TH><TD>\n\
<INPUT TYPE=submit VALUE=\"Submit\">&nbsp;&nbsp;\
<INPUT TYPE=reset VALUE=\"reset\">\n\
</TD></TR>\n\
</TABLE>\n\
</FORM>\n\
</BODY>\n\
</HTML>\n",
      SoftwareID, CgiLibEnvironmentPtr, SearchDna,
      CgiServerNamePtr, CgiServerNamePtr,
      SOFTWAREID, StartDateTime,
      CgiScriptNamePtr, cptr,
      StartDateTime+7, StartDateTime+7);
}

/****************************************************************************/
/*
Generate and return a C/Unix time in seconds from a 'dd-mmm-yyyy' string (e.g.
10-AUG-2002).  Returns zero if string invalid.
*/ 

int DayMonthYear (char *String)

{
   static char  *MonthNames [] = { "JAN", "FEB", "MAR", "APR",
                                   "MAY", "JUN", "JUL", "AUG", 
                                   "SEP", "OCT", "NOV", "DEC", NULL };
   int  cnt, day, month, year, ctime;
   char  *cptr;
   char  mstring [256];
   struct tm  tmStruct;

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

   if (Debug) fprintf (stdout, "DayMonthYear() |%s|\n", String);

   if (!String) return (0);

   for (cptr = String; *cptr; cptr++) *cptr = toupper(*cptr);
   day = year = 0;
   mstring[0] = '\0';
   cnt = sscanf (String, "%d-%3s-%d", &day, &mstring, &year);
   if (Debug) fprintf (stdout, "%d|%s|%d\n", day, mstring, year);
   if (cnt != 3) return (0);
   if (day < 1 || day > 31) return (0);
   if (year < 1990 || year > 2099) return (0);  /* now that's optimistic! */
   for (month = 0; MonthNames[month]; month++)
      if (strsame (mstring, MonthNames[month], -1)) break;

   memset (&tmStruct, 0, sizeof(tmStruct));
   tmStruct.tm_mday = day;
   tmStruct.tm_mon = month;
   tmStruct.tm_year = year - 1900;
   ctime = mktime (&tmStruct);
   if (Debug) fprintf (stdout, "ctime %u\n", ctime);

   if (ctime != -1) return (ctime);
   return (0);
}

/****************************************************************************/
/*
Turn the supplied string, for example "dd/aug/2002:dd:hh:mm +hhmm", into a
wilcarded date/time filter, such as "@@/aug/2002" and return a pointer to that
(sometimes modified) string.  If the string has not been changed from the
default value of "dd/mmm/yyyy:hh:mm:ss" then return NULL.
*/ 

char* DateTimeFilter (char *String)

{
   char  *cptr;

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

   if (Debug) fprintf (stdout, "DateTimeFilter() |%s|\n", String);

   if (!String) return (NULL);
   if (strsame (String, "dd/mmm/yyyy:hh:mm:ss", 20)) return (NULL);

   cptr = String;
   if (*cptr == 'd') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'd') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr != '/') return (String);
   if (*cptr) cptr++;

   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr != '/') return (String);
   if (*cptr) cptr++;

   if (*cptr == 'y') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'y') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'y') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'y') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr != ':') return (String);
   if (*cptr) cptr++;

   if (*cptr == 'h') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'h') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr != ':') return (String);
   if (*cptr) cptr++;

   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr != ':') return (String);
   if (*cptr) cptr++;

   if (*cptr == 's') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 's') *cptr = '*';
   if (*cptr) cptr++;
   if (!isspace(*cptr)) return (String);
   if (*cptr) cptr++;

   if (*cptr != '+' && *cptr != '-' && *(unsigned char*)cptr != 177)
      return (String);
   *cptr++ = '*';
   if (*cptr == 'h') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'h') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr == 'm') *cptr = '*';
   if (*cptr) cptr++;
   if (*cptr) return (String);

   if (cptr > String && !*cptr) cptr--;
   while (cptr > String && !isalnum(*cptr)) cptr--;
   while (*cptr && *cptr != '*') cptr++;
   if (*cptr == '*') cptr++;
   *cptr = '\0';

   return (String);
}

/****************************************************************************/
/*
Search against the command line file specification, passing each file found to
be processed.
*/ 

int SearchFileSpec (char *FileSpec)

{
   int  status,
        FileNameLength,
        Length;
   char  FileName [256],
         ExpFileName [256];
   struct FAB  SearchFab;
   struct NAM  SearchNam;

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

   if (Debug)
      fprintf (stdout, "SearchFileSpec() |%s|%s|\n", SearchDna, FileSpec);

   /* initialize the file access block */
   SearchFab = cc$rms_fab;
   SearchFab.fab$l_dna = SearchDna;
   SearchFab.fab$b_dns = strlen(SearchDna);
   SearchFab.fab$l_fna = FileSpec;
   SearchFab.fab$b_fns = strlen(FileSpec);
   SearchFab.fab$l_nam = &SearchNam;

   SearchNam = cc$rms_nam;
   SearchNam.nam$l_esa = ExpFileName;
   SearchNam.nam$b_ess = sizeof(ExpFileName)-1;
   SearchNam.nam$l_rsa = FileName;
   SearchNam.nam$b_rss = sizeof(FileName)-1;

   if (VMSnok (status = sys$parse (&SearchFab, 0, 0))) return (status);

   while (VMSok (status = sys$search (&SearchFab, 0, 0)))
   {
      *SearchNam.nam$l_ver = '\0';
      if (Debug) fprintf (stdout, "FileName |%s|\n", FileName);

      ProcessLogFile (FileName);

      FileCountTotal++;
      *SearchNam.nam$l_ver = ';';
   }

   return (status);
}

/****************************************************************************/
/*
Open the log file, read and process each record from it, close the file!
*/ 

int ProcessLogFile (char *FileName)

{
   BOOL  accepted,
         ShowLog;
   int  status;
   char  *cptr;
   char  HtmlLine [MAX_LINE_LENGTH+(MAX_LINE_LENGTH/4)],
         Line [MAX_LINE_LENGTH],
         LineBuffer [MAX_LINE_LENGTH];
   FILE  *FilePtr;
   stat_t  statBuffer;

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

   if (Debug) fprintf (stdout, "ProcessLogFile() |%s|\n", FileName);

   if (status = stat (FileName, &statBuffer) < 0)
   {
      if (Debug) fprintf (stdout, "stat() %d %%X%08.08X\n", status, vaxc$errno);
      AccessProblemCount++;
      status = vaxc$errno;
      return (status);
   }
   if (CtimeBefore && statBuffer.st_mtime > CtimeBefore)
   {
      LastModifiedIgnoredCount++;
      return (SS$_NORMAL);
   }
   if (CtimeSince && statBuffer.st_mtime < CtimeSince)
   {
      LastModifiedIgnoredCount++;
      return (SS$_NORMAL);
   }

   if (ShowProgress && !ViewRecords)
   {
      /* this works for both CLI and CGI (inside "<!-- -->"!) */
      if (ProgressCount == ProgressWrap)
      {
         ProgressCount = 0;
         fputc ('\n', stdout);
      }
      ProgressCount++;
      fputc ('+', stdout);
      fflush (stdout);
   }

   if (ShowLogFile)
      ShowLog = true;
   else
      ShowLog = false;

   RecordCount = 0;

   FilePtr = fopen (FileName, "r", "shr=get", "shr=put");
   if (!FilePtr)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fopen() %%X%08.08X\n", status);
      AccessProblemCount++;
      return (status);
   }

   while (fgets (Line, sizeof(Line), FilePtr))
   {
      RecordCount++;
      RecordCountTotal++;

      if (ViewRecords == VIEW_NONE)
      {
         /* again this works for both CLI and CGI (inside "<!-- -->"!) */
         if (ShowProgress && !(RecordCount % ShowProgress))
         {
            if (ProgressCount == ProgressWrap)
            {
               ProgressCount = 0;
               fputc ('\n', stdout);
            }
            ProgressCount++;
            fputc ('.', stdout);
            fflush (stdout);
         }
      }
      else
      if (ViewRecords == VIEW_ALL)
      {
         if (DoCliStats)
            OutputLogRecord (Line);
         else
         {
            CgiLibHtmlEscape (Line, -1, HtmlLine, sizeof(HtmlLine));
            OutputLogRecord (HtmlLine);
         }
      }
      else
         strcpy (LineBuffer, Line);

      if (ViewRecords == VIEW_LOG)
      {
         if (ShowLog)
         {   
            for (cptr = FileName; *cptr; cptr++);
            while (cptr > FileName && *cptr != ']' && *cptr != ':') cptr--;
            if (*cptr == ']' || *cptr == ':') cptr++;
            fprintf (stdout, "%s\n", cptr);
            ShowLog = false;
         }
      }

      if (Debug) fprintf (stdout, "|%s|\n", Line);
      accepted = ProcessLogRecord (Line);

      if (ViewRecords == VIEW_MATCH && accepted)
      {
         if (ShowLog)
         {   
            for (cptr = FileName; *cptr; cptr++);
            while (cptr > FileName && *cptr != ']' && *cptr != ':') cptr--;
            if (*cptr == ']' || *cptr == ':') cptr++;
            fprintf (stdout, "%s\n", cptr);
            ShowLog = false;
         }
         if (DoCliStats)
            OutputLogRecord (LineBuffer);
         else
         {
            CgiLibHtmlEscape (LineBuffer, -1, HtmlLine, sizeof(HtmlLine));
            OutputLogRecord (HtmlLine);
            fflush (stdout);
         }
      }
      else
      if (ViewRecords == VIEW_NOMATCH && !accepted)
      {
         if (ShowLog)
         {   
            for (cptr = FileName; *cptr; cptr++);
            while (cptr > FileName && *cptr != ']' && *cptr != ':') cptr--;
            if (*cptr == ']' || *cptr == ':') cptr++;
            fprintf (stdout, "%s\n", cptr);
            ShowLog = false;
         }
         if (DoCliStats)
            OutputLogRecord (LineBuffer);
         else
         {
            CgiLibHtmlEscape (LineBuffer, -1, HtmlLine, sizeof(HtmlLine));
            OutputLogRecord (HtmlLine);
            fflush (stdout);
         }
      }
   }

   fclose (FilePtr);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Output a single record from the log.  If client host name lookup is enabled
then check whether the client field is a dotted-decimal host address.  If it is
then lookup the host name and output that instead of the address.
*/

void OutputLogRecord (char *sptr)

{
   char  ch;
   char  *cptr, *zptr;

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

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

   if (!LookupRetry)
   {
      fputs (sptr, stdout);
      return;
   }

   /* check if it looks like an IPv4 address */
   for (cptr = sptr; *cptr && (isdigit(*cptr) || *cptr == '.'); cptr++);
   if (!isspace(*cptr))
      /* nope, check if it looks like an IPv6 address */
      for (cptr = sptr; *cptr && (isxdigit(*cptr) || *cptr == ':'); cptr++);

   if (!isspace(*cptr))
   {
      /* nope, assume it's a host name */
      fputs (sptr, stdout);
      return;
   }

   /* attempt to resolve the address string to a host name */
   for (zptr = sptr; *zptr && !isspace(*zptr); zptr++);
   ch = *zptr;
   *zptr = '\0';
   cptr = TcpIpLookupHostName (sptr, 0, NULL);
   *zptr = ch;
   if (!cptr)
   {
      /* didn't resolve, output the address */
      fputs (sptr, stdout);
      return;
   }

   /* output the resolved host name */
   fputs (cptr, stdout);
   /* look for the trailing space */
   while (*sptr && *sptr != ' ') sptr++;
   /* output the rest of the log ecord */
   fputs (sptr, stdout);
}

/*****************************************************************************/
/*
Process a single record (line) from the log file.
Common: 'client rfc891 authuser date/time request status bytes'
Combined: 'client rfc891 authuser date/time request status bytes referer agent'
*/

BOOL ProcessLogRecord (char *Line)

{
   BOOL  RejectRecord;
   int  ByteCount,
        Ip4Address;
   int  Ip6Address [4];
   char  *cptr, *sptr,
         *AuthUserPtr,
         *ClientPtr,
         *BytesPtr,
         *DateTimePtr,
         *QueryStringPtr,
         *ProtocolPtr,
         *RefererPtr,
         *RemoteIdentPtr,
         *RequestPtr,
         *StatusPtr,
         *UserAgentPtr;
   char  ch;
   char  EmptyString [1],
         Scratch [MAX_LINE_LENGTH];

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

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

   EmptyString[0] = '\0';

   cptr = Line;

   /* client */
   ClientPtr = cptr;
   while (*cptr && !isspace(*cptr)) cptr++;
   if (*cptr) *cptr++ = '\0';

   /* remote ident (RFC891) */
   while (*cptr && isspace(*cptr)) cptr++;
   RemoteIdentPtr = cptr;
   while (*cptr && !isspace(*cptr)) cptr++;
   if (*cptr) *cptr++ = '\0';

   /* authorized user */
   while (*cptr && isspace(*cptr)) cptr++;
   AuthUserPtr = cptr;
   while (*cptr && !isspace(*cptr)) cptr++;
   if (*cptr) *cptr++ = '\0';

   /* date/time */
   while (*cptr && *cptr != '[') cptr++;
   if (*cptr) cptr++;
   DateTimePtr = cptr;
   while (*cptr && *cptr != ']') cptr++;
   if (*cptr) *cptr++ = '\0';

   /* request ("method path?query protocol") */
   while (*cptr && *cptr != '\"') cptr++;
   if (*cptr) cptr++;
   RequestPtr = cptr;
   while (*cptr && *cptr != '\"') cptr++;
   if (*cptr) *cptr++ = '\0';

   /* HTTP response status */
   while (*cptr && isspace(*cptr)) cptr++;
   StatusPtr = cptr;
   while (*cptr && !isspace(*cptr)) cptr++;
   if (*cptr) *cptr++ = '\0';

   /* bytes transmitted */
   while (*cptr && isspace(*cptr)) cptr++;
   BytesPtr = cptr;
   while (*cptr && !isspace(*cptr)) cptr++;
   if (*cptr) *cptr++ = '\0';

   /* referer (only for COMBINED) */
   RefererPtr = NULL;
   while (*cptr && isspace(*cptr)) cptr++;
   if (*cptr == '\"')
   {
      cptr++;
      RefererPtr = cptr;
      while (*cptr && *cptr != '\"') cptr++;
   }
   else
   if (*cptr)
   {
      RefererPtr = cptr;
      while (*cptr && !isspace(*cptr)) cptr++;
   }
   if (*cptr) *cptr++ = '\0';

   /* user agent (only for COMBINED) */
   UserAgentPtr = NULL;
   while (*cptr && isspace(*cptr)) cptr++;
   if (*cptr == '\"')
   {
      cptr++;
      UserAgentPtr = cptr;
      while (*cptr && *cptr != '\"') cptr++;
   }
   else
   if (*cptr)
   {
      UserAgentPtr = cptr;
      while (*cptr && !isspace(*cptr)) cptr++;
   }
   if (*cptr) *cptr++ = '\0';

   if (!ClientPtr[0] ||
       !DateTimePtr[0] ||
       !RequestPtr[0] ||
       !StatusPtr[0] ||
       !BytesPtr)
   {
      SuspectRecordCountTotal++;
      return (false);
   }

   if (!RefererPtr && !UserAgentPtr)
      CommonLogRecordCount++;
   else
      CombinedLogRecordCount++;

   /* reset the pattern counter */
   FilterThisOut (NULL, NULL);

   RejectRecord = false;

   if (ClientFilterPtr)
   {
      if (LookupRetry)
      {
         cptr = TcpIpLookupHostName (ClientPtr, 0, NULL);
         if (cptr) ClientPtr = cptr;
      }
      if (FilterThisOut (ClientPtr, ClientFilterPtr))
      {
         ClientFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         ClientFilterAcceptCount++;
   }
   if (IpVersionFilter)
   {
      /* check if it looks like an IPv4 address */
      for (cptr = ClientPtr; isdigit(*cptr) || *cptr == '.'; cptr++);
      if (!*cptr)
      {
         /* looks like IPv4 */
         if (IpVersionFilter == 4)
            Ip4FilterAcceptCount++;
         else
         {
            Ip4FilterRejectCount++;
            if (!FilterOnAll) return (false);
            RejectRecord = true;
         }
      }
      else
      {
         /* check if it looks like an IPv6 address */
         for (cptr = ClientPtr;
              isxdigit(*cptr) || *cptr == ':' || *cptr == '-';
              cptr++);
         if (!*cptr)
         {
            /* looks like IPv6 */
            if (IpVersionFilter == 6)
               Ip6FilterAcceptCount++;
            else
            {
               Ip6FilterRejectCount++;
               if (!FilterOnAll) return (false);
               RejectRecord = true;
            }
         }
         else
         {
            /* assume it's a host name */
            cptr = TcpIpLookupAddress (ClientPtr, &Ip4Address, Ip6Address);
            if (IpVersionFilter == 4)
            {
               if (cptr)
               {
                  if (Ip4Address)
                     Ip4FilterAcceptCount++;
                  else
                  {
                     Ip4FilterRejectCount++;
                     if (!FilterOnAll) return (false);
                     RejectRecord = true;
                  }
               }
               else
               {
                  Ip4FilterRejectCount++;
                  if (!FilterOnAll) return (false);
                  RejectRecord = true;
               }
            }
            else
            if (IpVersionFilter == 6)
            {
               if (cptr)
               {
                  if (Ip4Address)
                  {
                     Ip6FilterRejectCount++;
                     if (!FilterOnAll) return (false);
                     RejectRecord = true;
                  }
                  else
                     Ip6FilterAcceptCount++;
               }
               else
               {
                  Ip6FilterRejectCount++;
                  if (!FilterOnAll) return (false);
                  RejectRecord = true;
               }
            }
         }
      }
   }
   if (RemoteIdentFilterPtr)
   {
      if (FilterThisOut (RemoteIdentPtr, RemoteIdentFilterPtr))
      {
         RemoteIdentFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         RemoteIdentFilterAcceptCount++;
   }
   if (AuthUserFilterPtr)
   {
      if (FilterThisOut (AuthUserPtr, AuthUserFilterPtr))
      {
         AuthUserFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         AuthUserFilterAcceptCount++;
   }
   if (DateTimeFilterPtr)
   {
      if (FilterThisOut (DateTimePtr, DateTimeFilterPtr))
      {
         DateTimeFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         DateTimeFilterAcceptCount++;
   }
   if (MethodFilterPtr)
   {
      for (cptr = RequestPtr; *cptr && *cptr != ' '; cptr++);
      /* terminate path at query string (should be necessary) */
      if (!*cptr) cptr = NULL;
      if (cptr) *cptr = '\0';
      if (FilterThisOut (RequestPtr, MethodFilterPtr))
      {
         MethodFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         MethodFilterAcceptCount++;
      if (cptr) *cptr = ' ';
   }
   QueryStringPtr = ProtocolPtr = NULL;
   if (PathFilterPtr)
   {
      /* skip over the method */
      for (cptr = RequestPtr; *cptr && *cptr != ' '; cptr++);
      while (*cptr && *cptr == ' ') cptr++;
      /* find the start of any query string */
      for (sptr = cptr; *sptr && *sptr != '?' && *sptr != ' '; sptr++);
      /* terminate path at query string */
      if (*sptr == '?')
         QueryStringPtr = sptr + 1;
      else
      {
         QueryStringPtr = EmptyString;
         if (*sptr == ' ') ProtocolPtr = sptr + 1;
      }
      ch = *sptr;
      *sptr = '\0';
      if (UrlDecodePath && *cptr)
      {
         strcpy (Scratch, cptr);
         cptr = Scratch;
         CgiLibUrlDecode (cptr);
      }
      if (FilterThisOut (cptr, PathFilterPtr))
      {
         PathFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         PathFilterAcceptCount++;
      *sptr = ch;
   }
   if (QueryFilterPtr)
   {
      if (QueryStringPtr)
         cptr = QueryStringPtr;
      else
      {
         /* find the start of any query string */
         for (cptr = RequestPtr; *cptr && *cptr != '?'; cptr++);
         if (*cptr) cptr++;
         QueryStringPtr = cptr;
      }
      for (sptr = cptr; *sptr && *sptr != ' '; sptr++);
      ch = *sptr;
      *sptr = '\0';
      if (UrlDecodeQuery && *cptr)
      {
         strcpy (Scratch, cptr);
         cptr = Scratch;
         CgiLibUrlDecode (cptr);
      }
      if (FilterThisOut (cptr, QueryFilterPtr))
      {
         QueryFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         QueryFilterAcceptCount++;
      *sptr = ch;
      if (*sptr == ' ')
         ProtocolPtr = sptr + 1;
      else
         ProtocolPtr = EmptyString;
   }
   if (ProtocolFilterPtr)
   {
      if (ProtocolPtr)
         cptr = ProtocolPtr;
      else
      {
         if (QueryStringPtr)
            cptr = QueryStringPtr;
         else
         {
            /* find the start of any query string */
            for (cptr = RequestPtr;
                 *cptr && *cptr != '?' && *cptr != ' ';
                 cptr++);
            if (*cptr == '?') cptr++;
         }
         /* find the end of the query string and the start of the protocol */
         while (*cptr && *cptr != ' ') cptr++;
      }
      if (*cptr == ' ') cptr++;
      if (FilterThisOut (cptr, ProtocolFilterPtr))
      {
         ProtocolFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         ProtocolFilterAcceptCount++;
   }
   if (RefererFilterPtr)
   {
      /* if the record did not have a COMBINED referer field then reject */
      if ((cptr = RefererPtr) && UrlDecodeReferer)
      {
         strcpy (Scratch, cptr);
         cptr = Scratch;
         CgiLibUrlDecode (cptr);
      }
      if (!cptr || FilterThisOut (cptr, RefererFilterPtr))
      {
         RefererFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         RefererFilterAcceptCount++;
   }
   if (ResponseFilterPtr)
   {
      if (!cptr || FilterThisOut (StatusPtr, ResponseFilterPtr))
      {
         StatusFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         StatusFilterAcceptCount++;
   }
   if (UserAgentFilterPtr)
   {
      /* if the record did not have a COMBINED user agent field then reject */
      if (!UserAgentPtr || FilterThisOut (UserAgentPtr, UserAgentFilterPtr))
      {
         UserAgentFilterRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         UserAgentFilterAcceptCount++;
   }

   ByteCount = atoi(BytesPtr);
   if (ResponseMinSize)
   {
      if (ByteCount < ResponseMinSize || !isdigit(*BytesPtr))
      {
         ResponseMinSizeRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         ResponseMinSizeAcceptCount++;
   }
   if (ResponseMaxSize)
   {
      if (ByteCount > ResponseMaxSize || !isdigit(*BytesPtr))
      {
         ResponseMaxSizeRejectCount++;
         if (!FilterOnAll) return (false);
         RejectRecord = true;
      }
      else
         ResponseMaxSizeAcceptCount++;
   }

   if (RejectRecord) return (false);

   RequestCountTotal++;
   ByteCountTotal += (float)ByteCount;
   switch (StatusPtr[0])
   {
      case '1' : StatusCodeCount[1]++; break;
      case '2' : StatusCodeCount[2]++; break;
      case '3' : StatusCodeCount[3]++; break;
      case '4' : StatusCodeCount[4]++; break;
      case '5' : StatusCodeCount[5]++; break;
      default  : StatusCodeCount[0]++;
   }
   switch (*(unsigned short*)RequestPtr)
   {
      /* unfiltered it still will be uppercase, filtered lower-case */
      case 'CO' : case 'co' : MethodConnectCount++; break;
      case 'DE' : case 'de' : MethodDeleteCount++; break;
      case 'GE' : case 'ge' : MethodGetCount++; break;
      case 'HE' : case 'he' : MethodHeadCount++; break;
      case 'PO' : case 'po' : MethodPostCount++; break;
      case 'PU' : case 'pu' : MethodPutCount++; break;
      default   : MethodUnknownCount++;
   }

   return (true);
}


/*****************************************************************************/
/*
Pre-compile regular expressions.  Report the first error by return false. 
Return true if all required compile ok.
*/

BOOL CompileFilters ()

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

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

   FilterThisOut (NULL, NULL);
   if (ClientFilterPtr) FilterThisOut (NULL, ClientFilterPtr);
   if (RemoteIdentFilterPtr) FilterThisOut (NULL, RemoteIdentFilterPtr);
   if (AuthUserFilterPtr) FilterThisOut (NULL, AuthUserFilterPtr);
   if (DateTimeFilterPtr) FilterThisOut (NULL, DateTimeFilterPtr);
   if (MethodFilterPtr) FilterThisOut (NULL, MethodFilterPtr);
   if (PathFilterPtr) FilterThisOut (NULL, PathFilterPtr);
   if (QueryFilterPtr) FilterThisOut (NULL, QueryFilterPtr);
   if (ProtocolFilterPtr) FilterThisOut (NULL, ProtocolFilterPtr);
   if (RefererFilterPtr) FilterThisOut (NULL, RefererFilterPtr);
   if (ResponseFilterPtr) FilterThisOut (NULL, ResponseFilterPtr);
   if (UserAgentFilterPtr) FilterThisOut (NULL, UserAgentFilterPtr);
   if (RegCompPatternPtr) return (false);
   return (true);
}

/*****************************************************************************/
/*
Return false if the pattern matches the string, true if it doesn't.  Keep an
array of pre-compiled regular expressions.
*/

BOOL FilterThisOut
(
char *StringPtr,
char *PatternPtr
)
{
   static int  PatternCount;
   static regex_t  CompiledPattern[REGEX_PATTERN_MAX];

   BOOL  NegateResult;
   int  retval;
   char  *pptr, *sptr, *tptr, *zptr;
   char  Scratch [256];
   regex_t  *pregptr;

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

   if (Debug)
      fprintf (stdout, "FilterThisOut() |%s|%s|\n",
               StringPtr ? StringPtr : "", PatternPtr ? PatternPtr : "");

   if (!StringPtr && !PatternPtr)
   {
      /* reset the pattern count */
      PatternCount = 0;
      return (true);
   }

   if (PatternCount >= REGEX_PATTERN_MAX) exit (SS$_BUGCHECK);
   pregptr = &CompiledPattern[PatternCount++];

   if (*PatternPtr == '!')
   {
      PatternPtr++;
      /* if we're going to say *not* this, there must be *something* there */
      if (StringPtr && !*StringPtr) return (true);
      NegateResult = true;
   }
   else
      NegateResult = false;

   if (!pregptr->buffer)
   {
      /* check for simple wildcard match */
      if (*PatternPtr != REGEX_CHAR)
      {
         /* simple wildcard match, create regex equivalent */
         zptr = (sptr = Scratch) + sizeof(Scratch)-2;
         /* anchor the start */
         *sptr++ = '^';
         for (pptr = PatternPtr; *pptr && sptr < zptr; pptr++)
         {
            switch (*pptr)
            {
               case '*' :
                  /* match any zero or more characters */
                  *sptr++ = '.';
                  if (sptr < zptr) *sptr++ = *pptr;
                  break;
               case '%' :
                  /* match any single character */
                  *sptr++ = '.';
                  break;
               case '?' :
                  /* match zero or any single character */
                  *sptr++ = '.';
                  if (sptr < zptr) *sptr++ = *pptr;
                  break;
               case '\\' :
               case '^' :
               case '$' :
               case '.' :
               case '+' :
               case '|' :
               case '{' :
               case '[' :
               case '(' :
                  /* meta-character, quote the character */
                  *sptr++ = '\\';
                  if (sptr < zptr) *sptr++ = *pptr;
                  break;
               default :
                  *sptr++ = *pptr;
            }
         }
         /* anchor the end */
         *sptr++ = '$';
         *sptr = '\0';
         PatternPtr = Scratch;
         if (Debug) fprintf (stdout, "|%s|\n", PatternPtr);
      }
      else
         PatternPtr++;

      /* compile the pattern */
      retval = regcomp (pregptr, PatternPtr, REGEX_C_FLAGS);
      if (retval)
      {
         /* compilation error */
         if (!RegCompPatternPtr)
         {
            /* note the first regex compilation error details */
            RegCompPatternPtr = PatternPtr;
            regerror (retval, pregptr,
                      RegCompErrorString, sizeof(RegCompErrorString));
            if (Debug)
               fprintf (stdout, "|%s|%s|\n",
                        RegCompPatternPtr, RegCompErrorString);
            RegCompErrorString[0] = tolower(RegCompErrorString[0]);
         }
         return (true);
      }
      if (!StringPtr) return (true);
   }

   /*
      Start with a light-weight character match.
      Even if regex is eventually required this will eliminate many
      strings on simple character comparison before more heavy-weight
      pattern matching needs to be called into play.
   */
   sptr = StringPtr;
   pptr = PatternPtr;
   if (*pptr == '^')
   {
      /* must be a regex pattern */
      pptr++;
      /* step over any start-of-line anchor seeing we're there already */
      if (*pptr == '^')
      {
         pptr++;
         /* if regex for empty string, then it's a match */
         if (*(unsigned short*)pptr == '$\0' && !*sptr)
            return (NegateResult ? true : false);
      }
   }
   while (*pptr && *sptr)
   {
      if (*(unsigned short*)pptr == '*\0') break;
      switch (*pptr)
      {
         /* "significant" characters when pattern matching */
         case '*' :
         case '^' :
         case '$' :
         case '.' :
         case '+' :
         case '?' :
         case '|' :
         case '{' :
         case '[' :
         case '(' :
         case '\\' :
            /* meta-character, quit now and perform a regex match */
            retval = regexec (pregptr, StringPtr, 0, NULL, REGEX_E_FLAGS);
            if (retval) return (NegateResult ? false : true);
            return (NegateResult ? true : false);
      }
      if (tolower(*pptr) != tolower(*sptr) && *pptr != '%') break;
      pptr++;
      sptr++;
   }

   /* if matched exactly then don't filter it out */
   if (!*pptr && !*sptr) return (NegateResult ? true : false);

   /* if the pattern ended in a trailing wildcard it matches, don't filter */
   if (*(unsigned short*)pptr == '*\0') return (NegateResult ? true : false);

   /* doesn't match! */
   return (NegateResult ? false : true);
}

/*****************************************************************************/
/*
Get the IP name using synchronous address-to-name Ipv4/Ipv6 lookup.
Supply either an IP address string, or an IPv4 or IPv6 address.
The IP address string must be terminated by white-space or a null.
The static host name buffer should not be modified by the calling routine.

(This code is *derived* from a similar functionality in [SRC.HTTPD]TCPIP.C)
*/

char* TcpIpLookupHostName
(
char *IpAddressString,
int Ip4Address,
int *Ip6AddressPtr
)
{
   /* this is one second delta */
   static unsigned long  RetryDelta [2] = { -10000000, -1 };

   static unsigned char ControlSubFunction [4] =
      { INETACP_FUNC$C_GETHOSTBYADDR, INETACP$C_TRANS, 0, 0 };
   static struct dsc$descriptor ControlSubFunctionDsc =
      { 4, DSC$K_DTYPE_T, DSC$K_CLASS_S, (char*)&ControlSubFunction };

   static unsigned short  LookupChannel;
   static char  HostName [127+1];
   static $DESCRIPTOR (HostNameDsc, HostName);

   int  status,
        RetryAttempts;
   unsigned short  HostNameLength;
   int  LocalIp6Address [4];
   char  *cptr, *sptr, *zptr;
   struct dsc$descriptor *dscptr;
   struct dsc$descriptor HostAddressDsc;
   IO_SB  LookupIOsb;

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

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

   /* assign a channel to the internet template device */
   if (!LookupChannel)
   {
      status = sys$assign (&TcpIpDeviceDsc, &LookupChannel, 0, 0);
      if (VMSnok (status)) exit (status);
   }

   if (!Ip6AddressPtr) Ip6AddressPtr = LocalIp6Address;

   if (IpAddressString)
   {
      Ip4Address = 0;
      memset (Ip6AddressPtr, 0, 16);
      status = TcpIpStringAddress (IpAddressString,
                                   &Ip4Address, Ip6AddressPtr);
      if (VMSnok (status)) return (NULL);
   }

   if (Ip4Address)
      cptr = TcpIpHostCache (NULL, &Ip4Address, NULL);
   else
      cptr = TcpIpHostCache (NULL, NULL, Ip6AddressPtr);
   if (cptr)
      if (*cptr == '?')
         return (NULL);
      else
         return (cptr);

   dscptr = (struct dsc$descriptor*)&HostNameDsc;
   dscptr->dsc$b_class = DSC$K_CLASS_S;
   dscptr->dsc$b_dtype = DSC$K_DTYPE_T;
   dscptr->dsc$w_length = sizeof(HostName)-1;
   dscptr->dsc$a_pointer = HostName;

   dscptr = &HostAddressDsc;
   dscptr->dsc$b_class = DSC$K_CLASS_S;
   dscptr->dsc$b_dtype = DSC$K_DTYPE_T;
   if (Ip4Address)
   {
      dscptr->dsc$w_length = 4;
      dscptr->dsc$a_pointer = (char*)&Ip4Address;
   }
   else
   if (Ip6AddressPtr)
   {
      dscptr->dsc$w_length = 16;
      dscptr->dsc$a_pointer = (char*)Ip6AddressPtr;
   }
   else
      exit (SS$_BUGCHECK);

#ifdef TCPIP_LOOKUP_HOST_NAME_RETRY
   RetryAttempts = TCPIP_LOOKUP_HOST_NAME_RETRY;
#else
   RetryAttempts = 10;
#endif

   while (RetryAttempts--)
   {
      status = sys$qiow (0, LookupChannel, IO$_ACPCONTROL,
                         &LookupIOsb, 0, 0,
                         &ControlSubFunctionDsc, &HostAddressDsc,
                         &HostNameLength, &HostNameDsc, 0, 0);
      if (Debug)
         fprintf (stdout, "sys$qiow() %%X%08.08X %%X%08.08X\n",
                  status, LookupIOsb.Status);

      if (VMSnok (status)) LookupIOsb.Status = status;
      if (VMSok (LookupIOsb.Status)) break;

      sys$schdwk (0, 0, &RetryDelta, 0);
      sys$hiber();
   }

   if (VMSok (LookupIOsb.Status)) 
   {
      HostName[HostNameLength] = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", HostName);
      /* set the cache entry */
      if (Ip4Address)
         TcpIpHostCache (HostName, &Ip4Address, NULL);
      else
         TcpIpHostCache (HostName, NULL, Ip6AddressPtr);
      return (HostName);
   }
   else
   {
      /* a cache entry indicating it couldn't be resolved */
      if (Ip4Address)
         TcpIpHostCache ("?", &Ip4Address, NULL);
      else
         TcpIpHostCache ("?", NULL, Ip6AddressPtr);
      return (NULL);
   }
}

/*****************************************************************************/
/*
Get the IP address using synchronous name-to-address Ipv4/Ipv6 lookup.
The host name string must be terminated by white-space or a null.

(This code is *derived* from a similar functionality in [SRC.HTTPD]TCPIP.C)
*/

char* TcpIpLookupAddress
(
char *HostName,
int *Ip4AddressPtr,
int *Ip6AddressPtr
)
{
   /* this is one second delta */
   static unsigned long  RetryDelta [2] = { -10000000, -1 };

   static unsigned char ControlSubFunction [4] =
      { INETACP_FUNC$C_GETHOSTBYNAME, INETACP$C_TRANS, 0, 0 };
   static struct dsc$descriptor AddressDsc =
      { 0, DSC$K_DTYPE_T, DSC$K_CLASS_S, 0};
   static struct dsc$descriptor ControlSubFunctionDsc =
      { 4, DSC$K_DTYPE_T, DSC$K_CLASS_S, (char*)&ControlSubFunction };

   static unsigned short  LookupChannel;

   int  status,
        RetryAttempts;
   unsigned short  AddressLength,
                   HostNameLength;
   char  *cptr, *sptr, *zptr;
   int  Bytes16 [4];
   struct dsc$descriptor *dscptr;
   struct dsc$descriptor HostAddressDsc;
   struct dsc$descriptor HostNameDsc;
   IO_SB  LookupIOsb;

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

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

   /* assign a channel to the internet template device */
   if (!LookupChannel)
   {
      status = sys$assign (&TcpIpDeviceDsc, &LookupChannel, 0, 0);
      if (VMSnok (status)) exit (status);
   }

   for (cptr = HostName; *cptr; cptr++);
   HostNameLength = cptr - HostName;

   /* first, IPv4 literal address */
   for (cptr = HostName; isdigit(*cptr) || *cptr == '.'; cptr++);
   if (*cptr)
   {
      /* if not then, IPv6 literal address */
      for (cptr = HostName;
           isxdigit(*cptr) || *cptr == ':' || *cptr == '-';
           cptr++);
   }
   if (!*cptr)
   {
      /*******************/
      /* literal address */
      /*******************/

      if (Ip4AddressPtr) *Ip4AddressPtr = 0;
      if (Ip6AddressPtr) memset (Ip6AddressPtr, 0, 16);
      status = TcpIpStringAddress (HostName, Ip4AddressPtr, Ip6AddressPtr);
      if (VMSok (status))
         return (HostName);
      else
         return (NULL);
   }

   cptr = TcpIpHostCache (HostName, Ip4AddressPtr, Ip6AddressPtr);
   if (cptr)
      if (*cptr == '?')
         return (NULL);
      else
         return (cptr);

   dscptr = (struct dsc$descriptor*)&HostNameDsc;
   dscptr->dsc$b_class = DSC$K_CLASS_S;
   dscptr->dsc$b_dtype = DSC$K_DTYPE_T;
   dscptr->dsc$w_length = HostNameLength;
   dscptr->dsc$a_pointer = HostName;

   dscptr = &HostAddressDsc;
   dscptr->dsc$b_class = DSC$K_CLASS_S;
   dscptr->dsc$b_dtype = DSC$K_DTYPE_T;
   /* give the full buffer and then check the returned length */
   dscptr->dsc$w_length = sizeof(Bytes16);
   dscptr->dsc$a_pointer = (char*)Bytes16;
   memset (&Bytes16, 0, sizeof(Bytes16));

#ifdef TCPIP_LOOKUP_HOST_NAME_RETRY
   RetryAttempts = TCPIP_LOOKUP_HOST_NAME_RETRY;
#else
   RetryAttempts = 10;
#endif

   while (RetryAttempts--)
   {
      status = sys$qiow (0, LookupChannel, IO$_ACPCONTROL,
                         &LookupIOsb, 0, 0,
                         &ControlSubFunctionDsc,
                         &HostNameDsc,
                         &AddressLength,
                         &HostAddressDsc, 0, 0);
      if (Debug)
         fprintf (stdout, "sys$qiow() %%X%08.08X %%X%08.08X\n",
                  status, LookupIOsb.Status);

      if (VMSnok (status)) LookupIOsb.Status = status;
      if (VMSok (LookupIOsb.Status)) break;

      sys$schdwk (0, 0, &RetryDelta, 0);
      sys$hiber();
   }

   if (VMSok(LookupIOsb.Status))
   {
      if (AddressLength != 4 && AddressLength != 16) exit (SS$_BUGCHECK);
      /* set an entry in the host cache, copy the address to the pointer */
      if (AddressLength == 4)
      {
         TcpIpHostCache (HostName, Bytes16, NULL);
         memcpy (Ip4AddressPtr, Bytes16, 4);
      }
      else
      {
         TcpIpHostCache (HostName, NULL, Bytes16);
         memcpy (Ip6AddressPtr, Bytes16, 16);
      }
   }
   else
   {
      /* a cache entry indicating it couldn't be resolved */
      if (AddressLength == 4)
         TcpIpHostCache ("?", Bytes16, NULL);
      else
         TcpIpHostCache ("?", NULL, Bytes16);
      return (NULL);
   }
}

/*****************************************************************************/
/*
Maintains and allows lookup of a host name/address Ipv4/Ipv6 cache.

To lookup name->address supply a non-NULL 'HostName' and pointer(s)
to 'Ip4AddressPtr' (an int) and 'Ip6AddressPtr' (an array of int).

To lookup address->name supply one of 'Ip4AddressPtr' or 'Ip6AddressPtr' and
a NULL 'HostName'.  A pointer to host name is returned, or NULL.

To set an entry, supply 'HostName' and one of 'Ip4AddressPtr' (an int) and
'Ip6AddressPtr' (an array of int).  Supplying a zero address cancels the entry.

The host name must be delimitted by white-space or a null.

(This code is *derived* from a similar functionality in [SRC.HTTPD]TCPIP.C)
*/

char* TcpIpHostCache
(
char *HostName,
int *Ip4AddressPtr,
int *Ip6AddressPtr
)
{
#define HOST_CACHE_CHUNK 64
#define HOST_CACHE_EXPIRE_SECONDS 600

   typedef struct _HOST_CACHE_ENTRY {
      char  HostName [127+1];
      int  Ip4Address;
      int  Ip6Address [4];
      int  ExpireSecond,
           HostNameLength;
   } HOST_CACHE_ENTRY;

   static int  Ip6AddressZero [4];

   static int  HostCacheCount,
               HostCacheMax;
   static HOST_CACHE_ENTRY  *HostCachePtr;

   int  cnt,
        HostNameLength;
   unsigned long  CurrentSecond;
   char  *cptr, *sptr, *zptr;
   HOST_CACHE_ENTRY  *hcptr;

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

   if (Debug)
      fprintf (stdout, "TcpIpHostCache() %d %d %d %d %d\n",
               HostCacheMax, HostCacheCount,
               HostName, Ip4AddressPtr, Ip6AddressPtr);

   if (!HostCachePtr)
   {
      HostCacheMax = HOST_CACHE_CHUNK;
      HostCachePtr = calloc (HostCacheMax, sizeof(HOST_CACHE_ENTRY));
   }

   time (&CurrentSecond);

   if (HostName && Ip4AddressPtr && Ip6AddressPtr)
   {
      /*******************/
      /* name to address */
      /*******************/

      for (cptr = HostName; *cptr; cptr++);
      HostNameLength = cptr - HostName;
      hcptr = HostCachePtr;
      for (cnt = 0; cnt < HostCacheCount; hcptr++, cnt++)
      {
         if (hcptr->HostNameLength != HostNameLength) continue;
         if (memcmp (hcptr->HostName, HostName, HostNameLength)) continue;
         if (hcptr->ExpireSecond > CurrentSecond)
         {
            /* not-expired hit! */
            if (Ip4AddressPtr) *Ip4AddressPtr = hcptr->Ip4Address;
            if (Ip6AddressPtr) memcpy (Ip6AddressPtr, hcptr->Ip6Address, 16);
            if (Debug) fprintf (stdout, "name->addr HIT\n");
            return (HostName);
         }
         /* expired */
         hcptr->HostNameLength = 0;
         if (Debug) fprintf (stdout, "name->addr EXPIRED\n");
         return (NULL);
      }
      if (Debug) fprintf (stdout, "name->addr MISS\n");
      return (NULL);
   }

   if (!HostName && (Ip4AddressPtr || Ip6AddressPtr))
   {
      /*******************/
      /* address to name */
      /*******************/

      hcptr = HostCachePtr;
      for (cnt = 0; cnt < HostCacheCount; hcptr++, cnt++)
      {
         if (!hcptr->HostNameLength) continue;
         if (Ip4AddressPtr && *Ip4AddressPtr != hcptr->Ip4Address) continue;
         if (Ip6AddressPtr && memcmp (Ip6AddressPtr, hcptr->Ip6Address, 16))
            continue;
         if (hcptr->ExpireSecond > CurrentSecond)
         {
            /* not-expired hit! */
            if (Debug) fprintf (stdout, "addr->name HIT\n");
            return (hcptr->HostName);
         }
         /* expired */
         hcptr->HostNameLength = 0;
         if (Debug) fprintf (stdout, "addr->name EXPIRED\n");
         return (NULL);
      }
      if (Debug) fprintf (stdout, "addr->name MISS\n");
      return (NULL);
   }

   if ((HostName && Ip4AddressPtr && !Ip6AddressPtr) ||
       (HostName && !Ip4AddressPtr && Ip6AddressPtr))
   {
      /*******************/
      /* set cache entry */
      /*******************/

      for (cptr = HostName; *cptr; cptr++);
      HostNameLength = cptr - HostName;
      /* check if there an equivalent entry already in the cache */
      hcptr = HostCachePtr;
      for (cnt = 0; cnt < HostCacheCount; hcptr++, cnt++)
      {
         if (hcptr->HostNameLength != HostNameLength) continue;
         if (memcmp (hcptr->HostName, HostName, HostNameLength)) continue;
         /* yes, already in the cache */
         if (Ip4AddressPtr && !*Ip4AddressPtr ||
             Ip6AddressPtr && memcmp (Ip6AddressPtr, Ip6AddressZero, 16))
         {
            /* cancel the entry */
            hcptr->HostNameLength = 0;
            if (Debug) fprintf (stdout, "RESET\n");
            return (NULL);
         }
         if (Debug) fprintf (stdout, "set ALREADY\n");
         return (NULL);
      }
      /* check for an entry available for reuse */
      hcptr = HostCachePtr;
      for (cnt = 0; cnt < HostCacheCount; hcptr++, cnt++)
      {
         if (!hcptr->HostNameLength) break;
         if (hcptr->ExpireSecond < CurrentSecond)
         {
            /* this one needs refreshing */
            hcptr->HostNameLength = 0;
            break;
         }
      }
      if (cnt >= HostCacheCount)
      {
         /* nothing available for reuse */
         if (HostCacheCount < HostCacheMax)
         {
            /* use the next entry */
            HostCacheCount++;
            if (Debug) fprintf (stdout, "set NEXT\n");
         }
         else
         {
            /* out of next entries, expand the cache */
            HostCacheMax += HOST_CACHE_CHUNK;
            HostCachePtr = calloc (HostCacheMax, sizeof(HOST_CACHE_ENTRY));
            hcptr = &HostCachePtr[HostCacheCount++];
            if (Debug) fprintf (stdout, "set EXPAND\n");
         }
      }
      /* populate the entry */
      zptr = (sptr = hcptr->HostName) + sizeof(hcptr->HostName)-1;
      for (cptr = HostName; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      hcptr->HostNameLength = sptr - hcptr->HostName;
      if (Ip4AddressPtr)
         hcptr->Ip4Address = *Ip4AddressPtr;
      else
         hcptr->Ip4Address = 0;
      if (Ip6AddressPtr)
         memcpy (hcptr->Ip6Address, Ip6AddressPtr, 16);
      else
         memset (hcptr->Ip6Address, 0, 16);
      hcptr->ExpireSecond = CurrentSecond + HOST_CACHE_EXPIRE_SECONDS;
      if (Debug) fprintf (stdout, "SET\n");
      return (HostName);
   }

   exit (SS$_BUGCHECK);
}

/*****************************************************************************/
/*
Convert an IPv4 dotted-decimal or IPv6 hexadecimal format (normal or
compressed) string into an appropriate address.  The address must be delimited
by white-space or a null.  The white-space criterion is important in any
modification from TCPIP.C!

(This code is *derived* from a similar functionality in [SRC.HTTPD]TCPIP.C)
*/

int TcpIpStringAddress
(
char *String,
int *Ip4AddressPtr,
int *Ip6AddressPtr
)
{
   int  cnt, idx,
        Ip4Address;
   int  Ip4Octets [4];
   unsigned short  Ip6Address [8];
   unsigned short  *ip6ptr;
   unsigned int  uint;
   unsigned int  Ip6Octets [10];  /* well sort-of */
   char  *cptr, *sptr, *zptr;

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

   if (Debug) fprintf (stdout, "TcpIpStringAddress() |%s|\n", String);

   if (Ip4AddressPtr) *Ip4AddressPtr = 0;
   if (Ip6AddressPtr) memset (Ip6AddressPtr, 0, 16);

   /* will reach end-of-string if it's an IPv4 address */
   for (cptr = String; isdigit(*cptr) || *cptr == '.'; cptr++);

   if (!*cptr)
   {
      /********/
      /* IPv4 */
      /********/

      memset (Ip4Octets, 0, sizeof(Ip4Octets));
      cnt = sscanf (String, "%d.%d.%d.%d",
                    &Ip4Octets[0], &Ip4Octets[1],
                    &Ip4Octets[2], &Ip4Octets[3]);
      if (cnt != 4) return (SS$_ENDOFFILE);
      Ip4Address = 0;
      for (idx = 0; idx <= 3; idx++)
      {
         if (Ip4Octets[idx] < 0 || Ip4Octets[idx] > 255)
            return (SS$_ENDOFFILE);
         Ip4Address |= Ip4Octets[idx] << idx * 8;
      }
      if (Debug) fprintf (stdout, "IPv4 %08.08X\n", Ip4Address);
      *Ip4AddressPtr = Ip4Address;
      return (SS$_NORMAL);
   }

   /********/
   /* IPv6 */
   /********/

   memset (Ip4Octets, 0, sizeof(Ip4Octets));
   memset (Ip6Octets, 0, sizeof(Ip6Octets));

   /*
      _normal_                       _compressed_
      1070:0:0:0:0:800:200C:417B     1070::800:200C:417B
      0:0:0:0:0:0:13.1.68.3          ::13.1.68.3
      0:0:0:0:0:FFFF:129.144.52.38   ::FFFF:129.144.52.38
      _hyphen-variant_
      1070-0-0-0-0-800-200C-417B     1070--800-200C-417B
      0-0-0-0-0-0-13.1.68.3          --13.1.68.3
      0-0-0-0-0-FFFF-129.144.52.38   --FFFF-129.144.52.38
   */

   idx = 0;
   zptr = "";
   cptr = String;
   while (*cptr)
   {
      if (idx > 7) return (SS$_ENDOFFILE);
      /* look ahead at the next delimiter */
      for (sptr = cptr; isxdigit(*sptr); sptr++);
      if (*sptr == ':' || (!*sptr && *zptr == ':') ||
          *sptr == '-' || (!*sptr && *zptr == '-'))
      {
         /* IPv6 (or variant) syntax */
         uint = (unsigned long)strtol (cptr, NULL, 16);
         if (uint > 0xffff) return (SS$_ENDOFFILE);
         /* network byte-order */
         Ip6Octets[idx] = (uint >> 8) | (uint << 8);
         idx++;
         if (*(unsigned short*)sptr == '::' ||
             *(unsigned short*)sptr == '--')
         {
            /* indicate the ellipsis zeroes */
            Ip6Octets[idx] = 0xffffffff;
            idx++;
            sptr++;
         }
      }
      else
      if (*sptr == '.' || (!*sptr && *zptr == '.'))
      {
         /* dropped into dotted-decimal, IPv4 compatible syntax */
         cnt = sscanf (cptr, "%d.%d.%d.%d",
                       &Ip4Octets[3], &Ip4Octets[2],
                       &Ip4Octets[1], &Ip4Octets[0]);
         if (cnt != 4) return (SS$_ENDOFFILE);
         while (isdigit(*cptr) || *cptr == '.') cptr++;
         if (*cptr) return (SS$_ENDOFFILE);
         if (Ip4Octets[0] < 0 || Ip4Octets[0] > 255) return (SS$_ENDOFFILE);
         if (Ip4Octets[1] < 0 || Ip4Octets[1] > 255) return (SS$_ENDOFFILE);
         if (Ip4Octets[2] < 0 || Ip4Octets[2] > 255) return (SS$_ENDOFFILE);
         if (Ip4Octets[3] < 0 || Ip4Octets[3] > 255) return (SS$_ENDOFFILE);
         Ip6Octets[idx++] = (Ip4Octets[3] << 8) | Ip4Octets[2];
         Ip6Octets[idx++] = (Ip4Octets[1] << 8) | Ip4Octets[0];
         break;
      }
      else
         return (SS$_ENDOFFILE);
      cptr = zptr = sptr;
      if (*cptr) cptr++;
   }

   memset (ip6ptr = Ip6Address, 0, sizeof(Ip6Address));
   cnt = 9 - idx;
   for (idx = 0; idx < 8; idx++)
   {
      if (Ip6Octets[idx] == 0xffffffff)
      {
         if (cnt < 0) return (SS$_ENDOFFILE);
         while (cnt--) ip6ptr++;
      }
      else
         *ip6ptr++ = Ip6Octets[idx];
   }

   if (Debug)
      fprintf (stdout,
"IPv6 %04.04X%04.04X%04.04X%04.04X%04.04X%04.04X%04.04X%04.04X\n",
               Ip6Address[7], Ip6Address[6], Ip6Address[5], Ip6Address[4], 
               Ip6Address[3], Ip6Address[2], Ip6Address[1], Ip6Address[0]);
   memcpy (Ip6AddressPtr, Ip6Address, 16);
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.

(This code is *derived* from a similar functionality in [SRC.HTTPD]TCPIP.C)
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

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

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

   if (!(clptr = getenv ("QDLOGSTATS$PARAM")))
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* if CSWS (Apache) */
   if (getenv ("APACHE$SHARED_SOCKET"))
   {
      /* look for something non-space outside of quotes */
      for (cptr = clptr; *cptr; cptr++)
      {
         if (isspace(*cptr)) continue;
         if (*cptr != '\"') break;
         cptr++;
         while (*cptr && *cptr != '\"') cptr++;
         if (*cptr) cptr++;
      }
      /* if nothing outside of quotes then ignore the command line */
      if (!*cptr) return;
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (getenv ("WWWEXEC_RUNDOWN_STRING"))
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         SkipParameters--;
         continue;
      }

      if (strsame (aptr, "/ALL", -1))
      {
         FilterOnAll = true;
         continue;
      }
      if (strsame (aptr, "/AUTHUSER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/BEFORE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         LastModifiedBeforePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CLIENT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/DATETIME=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/DECODE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (!*cptr)
         {
            UrlDecodePath = UrlDecodeQuery = UrlDecodeReferer = true;
            continue;
         }
         if (*cptr == '(') cptr++;
         while (*cptr && *cptr != ')')
         {
            while (*cptr == ',') cptr++;
            if (*cptr) cptr++;
            if (toupper(*cptr) == 'P')
               UrlDecodePath = true;
            else
            if (toupper(*cptr) == 'Q')
               UrlDecodeQuery = true;
            else
            if (toupper(*cptr) == 'R')
               UrlDecodeReferer = true;
            else
            {
               for (sptr = cptr; *sptr && *sptr != ',' && *sptr != ')'; sptr++);
               *sptr = '\0';
               fprintf (stdout, "%%%s-E-INVKEYW, invalid keyword\n \\%s\\\n",
                        Utility, cptr);
               exit (STS$K_ERROR | STS$M_INHIB_MSG);
            }
            while (*cptr && *cptr != ',' && *cptr != ')') cptr++;
         }
         continue;
      }
      if (strsame (aptr, "/IP=", -1))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (isdigit(*cptr))
            IpVersionFilter = atoi(cptr);
         else
         {
            fprintf (stdout, "%%%s-E-IVKEYW, unrecognized keyword\n \\%s\\\n",
                     Utility, cptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }
      if (strsame (aptr, "/LOG", -1))
      {
         ShowLogFile = true;
         continue;
      }
      if (strsame (aptr, "/LOOKUP=", -1))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (isdigit(*cptr))
            LookupRetry = atoi(cptr);
         else
            LookupRetry = 0;
         continue;
      }
      if (strsame (aptr, "/METHOD=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/OUTPUT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         OutputPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/PATH=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/PROGRESS=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ShowProgress = atoi(cptr);
         if (ShowProgress <= 0) ShowProgress = PROGRESS_RECORDS;
         continue;
      }
      if (strsame (aptr, "/QUERY=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/REFERER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/REMOTEID=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/RESPONSE", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/SINCE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         LastModifiedSincePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/SIZE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         while (*cptr == '=') cptr++;
         while (*cptr == '(') cptr++;
         while (*cptr)
         {
            if (strsame (cptr, "MIN=", 4))
            {
               cptr += 4;
               ResponseMinSize = atoi(cptr);
               while (isdigit(*cptr)) cptr++;
               if (tolower(*cptr) == 'k') ResponseMinSize *= 1000;
               if (toupper(*cptr) == 'M') ResponseMinSize *= 1000000;
            }
            else
            if (strsame (cptr, "MAX=", 4))
            {
               cptr += 4;
               ResponseMaxSize = atoi(cptr);
               while (isdigit(*cptr)) cptr++;
               if (tolower(*cptr) == 'k') ResponseMaxSize *= 1000;
               if (toupper(*cptr) == 'M') ResponseMaxSize *= 1000000;
            }
            else
            {
               fprintf (stdout, "%%%s-E-IVKEYW, unrecognized keyword\n \\%s\\\n",
                        Utility, cptr);
               exit (STS$K_ERROR | STS$M_INHIB_MSG);
            }
            while (*cptr && *cptr != ',' && *cptr != ')') cptr++;
            while (*cptr == ',' || *cptr == ')') cptr++;
         }
         continue;
      }
      if (strsame (aptr, "/SOFTWAREID", 4) ||
          strsame (aptr, "/VERSION", 4))
      {
         fprintf (stdout, "%%%s-I-SOFTWAREID, %s\n%s\n",
                  Utility, SoftwareID, CopyrightInfo);
         exit (SS$_NORMAL);
      }
      if (strsame (aptr, "/USERAGENT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         continue;
      }
      if (strsame (aptr, "/VIEW=", 4))
      {
         ViewRecords = VIEW_MATCH;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         if (strsame (cptr, "ALL", 3))
            ViewRecords = VIEW_ALL;
         else
         if (strsame (cptr, "LOG", 3))
            ViewRecords = VIEW_LOG;
         else
         if (strsame (cptr, "MATCH", 3))
            ViewRecords = VIEW_MATCH;
         else
         if (strsame (cptr, "NOMATCH", 3))
            ViewRecords = VIEW_NOMATCH;
         else
         {
            fprintf (stdout, "%%%s-E-IVKEYW, unrecognized keyword\n \\%s\\\n",
                     Utility, cptr);
            exit (STS$K_ERROR | STS$M_INHIB_MSG);
         }
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!LogFileSpec[0])
      {
         sptr = LogFileSpec;
         for (cptr = aptr; *cptr; *sptr++ = toupper(*cptr++));
         *sptr = '\0';
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/*****************************************************************************/
/*
Get the value of a system-level (only) logical name.  Returns a pointer to
dynamic storage containing the null-terminated string, or NULL if a problem.
A *little* like 'getenv()' but only from the system logical table - serves much
the same purpose anyway.
*/

char* SysTrnLnmLnmSystem (char *LogicalName)

{
   static unsigned short  Length;
   static char  LogicalValue [256];
   static $DESCRIPTOR (LogicalNameDsc, "");
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(LogicalValue)-1, LNM$_STRING, LogicalValue, &Length },
      { 0,0,0,0 }
   };

   int  status;
   char  *sptr;

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

   if (Debug) fprintf (stdout, "SysTrnLnmLnmSystem() |%s|\n", LogicalName);

   LogicalNameDsc.dsc$w_length = strlen(LogicalName);
   LogicalNameDsc.dsc$a_pointer = LogicalName;

   status = sys$trnlnm (0, &LnmSystemDsc, &LogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (NULL);

   LogicalValue[Length] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", LogicalValue);
   if (!(sptr = calloc (1, Length+1))) exit (vaxc$errno);
   memcpy (sptr, LogicalValue, Length+1);
   return (sptr);
}

/*****************************************************************************/
/*
Return a pointer to the message string corresponding to the supplied VMS status
value.
*/

char* SysGetMsg (int VmsStatus)

{
   static char  Message [256];

   int  status;
   short int  Length;
   $DESCRIPTOR (MessageDsc, Message);

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

   Message[0] = '\0';
   if (VmsStatus)
   {
      /* text component only */
      sys$getmsg (VmsStatus, &Length, &MessageDsc, 1, 0);
      Message[Length] = '\0';
   }
   if (Message[0])
      return (Message);
   else
      return ("(internal error)");
}
 
/*****************************************************************************/
/*
Because it can be installed with privileges (for CLI usage) ...
*/ 

void NeedsPrivilegedAccount ()

{
   static unsigned long  PrivAcctMask [2] = { PRV$M_SETPRV | PRV$M_SYSPRV, 0 };

   static long  Pid = -1;
   static unsigned long  JpiAuthPriv [2];

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      void  *buf_addr;
      void  *ret_len;
   }
      JpiItems [] =
   {
      { sizeof(JpiAuthPriv), JPI$_AUTHPRIV, &JpiAuthPriv, 0 },
      {0,0,0,0}
   };

   int  status;

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

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

   status = sys$getjpiw (0, &Pid, 0, &JpiItems, 0, 0, 0);
   if (VMSnok (status)) exit (status);

   if (!(JpiAuthPriv[0] & PrivAcctMask[0])) exit (SS$_NOSYSPRV);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

BOOL strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   /*********/
   /* begin */
   /*********/

   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

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

int ShowHelp ()

{
   fputs (
"Usage for Quick and Dirty LOG STATisticS\n\
\n\
$ QDLOGSTATS <log-file-spec> [<filters>] [<other qualifiers>]\n\
\n\
Utility to extract very elementary statistics from Web server common/combined\n\
format log files.  A number of filters allow subsets of the log contents to be\n\
selected using simple wildcard expressions.  Strings are NOT case-sensitive.\n\
Optionally, log file records can be viewed as processed, or a simple progress\n\
indicator can be displayed (\"+\" for each file, \".\" per 1000 records thereof).\n\
\n\
/ALL /AUTHUSER=filter /BEFORE=time /CLIENT=filter /DATETIME=filter\n\
/DECODE[=keyword] /LOG /LOOKUP[=integer] /METHOD=filter /OUTPUT=file\n\
/PATH=filter /PROGRESS[=integer] /PROTOCOL=filter /QUERY=filter\n\
/REFERER=filter /RESPONSE=filter /SINCE=time /SIZE=[MIN=,MAX=] /SOFTWAREID\n\
/USERAGENT=filter /VIEW[=MATCH(D)|ALL|NOMATCH|LOG]\n\
\n\
$ QDLOGSTATS == \"$dir:QDLOGSTATS\"\n\
$ QDLOGSTATS HT_LOGS:*.LOG /PROGRESS\n\
$ QDLOGSTATS HT_LOGS:*.LOG /VIEW /PATH=\"/wasd/*.zip\"\n\
$ QDLOGSTATS HT_LOGS:*NOV*ACCESS* /PATH=\"/CGI-BIN/*\" /QUERY=\"-{-}\"\n\
$ QDLOGSTATS HT_LOGS:*ACCESS*.LOG /METHOD=POST /DATE=\"*/NOV/2000/*\"\n\
$ QDLOGSTATS HT_LOGS:*.LOG /SINCE=01-FEB-2002 /USERAGENT=*MOZILLA*X11*\n\
\n", stdout);

   return (SS$_NORMAL);
}

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

