/*****************************************************************************/
/*
                                ProxyCache.c

*************
** CAUTION **
*************

THIS MODULE IS TASK-ORIENTED, NOT REQUEST-ORIENTED.

That is, most of the functions take a pointer to proxy task rather than a
pointer to request as do other modules. The reason is simple. Many of these
functions are designed for use independently of a request.


INTRODUCTION
------------
Implements a basic HTTP proxy disk caching service.  The design always trades
simplicity off against efficiency and elegance.  Cache management is performed
by the PROXYMAINT.C module.  It works intimately with this module to provide
routine and reactive purging (deletion) of cache files to maintain device free
space and currency of cache content.

HTTP/1.1 behaviour is based on descriptions in RFC2616 (June, 1999).
Although RFC2616 has been extensively referenced the WASD proxy cache should
not be considered fully HTTP/1.1 compliant.  It is more of an effort to not
flagrantly flout HTTP/1.1 caching requirements.  Simplicity has generally been
chosen over RFC SHALLs (largely due to development time available and the size
of the potential user base).


CACHE CRITERIA
--------------
HTTP/1.1 cache behaviour is based on descriptions in RFC2616 (June, 1999).
The Squid FAQ (http://www.squid-cache.org/ - "How come some objects do not get
cached?") has also been used as a guide to what can and can't be cached under
given circumstances.

Proxied requests COULD ONLY have been cached if THE REQUEST ...

  o  uses the GET method
  o  does not contain a query string
  o  is HTTP/1.n compliant (i.e. not HTTP/0.9)
  o  does not contain an "Authorization:" header field
     (and the response does not contain a "Cache-Control: public" field)

Successful responses WILL ONLY be cached if THE RESPONSE ...

  o  status code 200 (success)
                 203 (non-authoritative)
                 300 (multiple choice)
                 301 (moved permanently)
                 410 (gone)
  o  contains a "Last-Modified:" header field
  o  one or more hours since the last modification
  o  does not contain restrictive cache control
                 "Pragma: no-cache" field (HTTP/1.0)
                 "Cache-Control: no-cache, no-store, private" (/1.1)
  o  "Vary:" header field does not contain a "*" or "accept[-...]"
  o  does not exceed a configuration parameter in size
  o  is HTTP/1.n compliant (i.e. not HTTP/0.9)

Unsuccessful (negative) responses will be cached if ...

  o  [ProxyCacheNegativeSeconds] is non-zero
  o  status code 305 (use proxy)
                 400 (bad request)
                 403 (forbidden)
                 404 (not found)
                 405 (method not allowed)
                 414 (request URI too large)
                 500 (internal server error)
                 501 (not implemented)
                 502 (bad gateway)
                 503 (service unavailable)
                 504 (gateway timeout)
  o  does not contain restrictive cache control
                 "Pragma: no-cache" field (HTTP/1.0)
                 "Cache-Control: no-cache, no-store, private" (/1.1)

"Content-Length:" is only used (if available) to check load integrity.

Configuration parameter for maximum cachable response size.  This applies to
response without content-length header fields, the in-progress caching is just
aborted if the response grows to exceed this size.

  [ProxyCacheFileKbytesMax] ... maximum number of Kbytes for any one response


FILE CACHE
----------
The file cache is supported using ODS-2 conventions.  This does not mean that
it cannot be located on an ODS-5 device, just that the directory depth,
structure and file naming must be ODS-2 compliant (which it is by default). 
There seems little advantage in supporting extended file specifications for
this functionality so I'm spending the time elsewhere!

One file is used for each unique request.  A request is made unique by creating
a request identifier using the request host name, the port, and the request
path (excluding any query string of course).  This is essentially the URL.  A
unique file name is generated for each of these unique requests by generating
an MD5 digest using the identifier.  This, for all intents and purpose (at
least according to RSA), generates a unique "fingerprint" of the identifier's
text.  No other checking is required to ensure the file represents the request!

A quotation from RFC1321 (April 1992):

  The algorithm takes as input a message of arbitrary length and produces
  as output a 128-bit "fingerprint" or "message digest" of the input. It
  is conjectured that it is computationally infeasible to produce two
  messages having the same message digest ...

The digest is 16 bytes (128 bits) represented as 32 hexadecimal digits and
looks like this:

  2BF641901B1661D3345C4D058A0F390F

Use of the MD5 algorithm has been shown in practice to produce a *very* even
distribution of file names.

To help reduce per-directory congestion the cached files are distributed
throughout a number of subdirectories.

Two cache directory organizations are currently available.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These are selected using the [ProxyCacheDeviceDirOrg] configuration directive.

The first (and default) provides a single level structure with a possible 256
directories at the top level and files organized immediate below these. The
directory name for any file is generated using the first two hexadecimal
digits, hence subdirectory names range from [00], [01], [02] to [FF]! The other
30 digits comprise the file name, with an extension of ".HTC". Subirectories
are created by the server as required. Hence a proxy cache file name looks
something like: 

  WASD_CACHE_ROOT:[2B]F641901B1661D3345C4D058A0F390F.HTC

This is the original structure and works well where files do not exceed 256 per
directory for a total of approximately 65,000 files.  For versions of VMS prior
to V7.2 exceeding 256 files per directory incurs a significant performance
penalty for some directory operations. 

The second organization involves two levels of directory, each with a maximum
of 64 directories.  This is accomplished by using the leading three hexadecimal
digits (12 bits), the first becomes the first character of the top-level
directory name, the upper two bits of the second represented byte the
hexadecimal equivalent of the second character, the lower two bits of the
hexadecimal equivalent of the first character of the second directory and the
third character the second character of the second directory.  A file in this
structure cache might look like:

  WASD_CACHE_ROOT:[22.3F]641901B1661D3345C4D058A0F390F.HTC

Here the first three characters, "2BF" or 0010+1011+1111, as a bit-pattern,
become "22", or 0010+10, for the first directory name, and "3F", or 11+1111,
for the second directory name.  Hence the first three hexdecimal characters of
the MD5 digest become the directory path [22.3F].  Directory names will begin
at [00.00], [00.01], [00.02] and range through to [F3.3D], [F3.3E], [F3.3F].
This allows for approximately 1,000,000 files before encountering the 256 file
per directory issue.

No "database" or other record is maintained of the files in the cache. All
access is via the unique MD5 "fingerprint" of the request. Hence it is easy to
reclaim disk space by manually deleting files. Cache files should not of
course be renamed to other subdirectories.


CACHE FILE STRUCTURE
--------------------
Three 32 bit integers prefix the cache file; cache version number, request URL
length (length of "http://node.domain:port/path"), response header length (that
received from the remote server).  Following these three integers is a null
terminated string, the URL string (length, including terminating null in second
integer). Then the response header (not null-terminated), immediately followed
by the response body (not null-terminated) to the end-of-file (which delimits
the body ... and response header for that matter).  The following diagram
illustrates this layout.

   16       (URL)12             8             4             0   VBN 1
    +-------------+-------------+-------------+-------------+
    | uu uu uu uu |  HEADER LEN |     URL LEN |     VERSION |
    +-------------+-------------+-------------+-------------+
    | uu uu uu uu | uu uu uu uu | uu uu uu uu | uu uu uu uu |
    +-------------+-------------+-------------+-------------+
    .
    .                           (HEADER)
    +-------------+-------------+-------------+-------------+
    | hh hh hh hh | hh hh hh hh | hh hh \0 uu | uu uu uu uu |
    +-------------+-------------+-------------+-------------+
    | hh hh hh hh | hh hh hh hh | hh hh hh hh | hh hh hh hh |
    +-------------+-------------+-------------+-------------+
    .
    .               (BODY)
    +-------------+-------------+-------------+-------------+
    | bb bb bb bb | bb bb hh hh | hh hh hh hh | hh hh hh hh |
    +-------------+-------------+-------------+-------------+
    | bb bb bb bb | bb bb bb bb | bb bb bb bb | bb bb bb bb |
    +-------------+-------------+-------------+-------------+
    .
    .
    +-------------+-------------+-------------+-------------+
    |             |             |       bb bb | bb bb bb bb |
    +-------------+-------------+-------------+-------------+
                                    EOF^                        VBN n


DISK SPACE USAGE
----------------
To simplify cache management and processing overhead the proxy cache is allowed
to use up to a configuration maximum percentage of the particular device it is
located on.  That is, it is allowed to continue to use free space until the
device reaches a specified percentage used.  At that point it deletes cache
files until another, lower device usage percentage is reached.  Hence the cache
will grow using available free space, unless that free space is called-on for
some other purpose, at which time it reduces it's own usage.  The following
configuration parameters control this behaviour.

  [ProxyCacheDeviceMaxPercent] ...... maximum device percentage in use 
  [ProxyCacheDevicePurgePercent] ... when purging usage reduce by this


CACHE FILE MANAGEMENT
---------------------
Cache file deletion takes two forms.  The first is ROUTINE, where files that
have not been accessed within specified limits are deleted.  The second is
remedial, a PURGE, where cache space usage is reaching it's limit and files
need to be deleted.  The following two configuration parameters control the
minutes between each of these events.

  [ProxyCacheRoutineHourOfDay] ..... routine delete of non-accessed files
                                     (if 24 done constantly in the background)
  [ProxyCacheDeviceCheckMinutes] ... check if the cache device needs purging

In the first, ROUTINE form the cache files are scanned looking for those that
exceed the configuration parameters for maximum period since last access, which
are then deleted (see [ProxyCachePurgeList] described below).

The second form, a PURGE of cache space, the files are scanned using the
[ProxyCachePurgeList] parameter above, working from the greatest to least
number of hours in the steps provided.  At each scan files not accessed within
that period are deleted.  Each few files deleted the device free space is
checked for having reached the lower purge percentage limit, at which point the
scan terminates.

  [ProxyCachePurgeList] ... series of comma-separated integers

This parameter has as it's input a series of comma-separated integers
representing a series of purges to the hour a file was last accessed. In this
way the cache can be progressively reduced until percentage usage targets are
realized.  (Note that this parameter specifies hours in the reverse order to
[ProxyCacheReloadList].)  Such a parameter would be specified as follows,

  [ProxyCachePurgeList] 168,48,24,8,0

meaning the purge would first delete files not accessed in the last week, then
not for the last two days, then the last twenty-four hours, then eight, then
finally all files.  The largest of the specified periods (in this case 168) is
also used as the limit for the ROUTINE scan and file delete.

Once the target reduction percentage is reached the purge stops. During the
purge operation, and until the target reduction is reached, cache files are not
created.  Unless the target reduction percentage can be reached no more cache
files will be created.  Even when cache file cannot be created for any reason
proxy serving still continues transparently to the clients.

CACHE FILES CAN BE MANUALLY DELETED AT ANY TIME (FROM THE COMMAND LINE, AND
PROVIDED THEY ARE NOT LOCKED FOR ANY REASON) WITHOUT DISTURBING THE
PROXY-CACHING SERVER AND WITHOUT REBUILDING ANY DATABASES.

When deleting, the /BEFORE= qualifier can be used, with /CREATED being the
document's last-modified date, /BACKUP being the last time it was loaded,
/REVISED the last time the file was accessed (used to supply a request) and
/EXPIRES any expiry date/time.


CACHE MAINTENANCE COMMENT
-------------------------
This cache design is obviously very file system intensive (not necesarily an
optimal choice with the notoriously slow VMS file system).  The trade-off is of
course simplicity of implementation and management.  Let the file system do the
database management for it, with nothing to go wrong, maintain or rebuild
outside of that ;^)  The use of the MD5 algorithm to generate unique cache file
names makes all this straight-forward.


FILE HEADER DT FIELDS
---------------------
(for v9.0.0ff cache files, different to the previous version - v6.0.0ff)

BDT - backup contains the loaded date/time
CDT - creation is used to hold a responses last-modified date/time
RDT - revision contains the date/time the file was last accessed
EDT - for cached responses, set to any supplied "Expires:" date/time
      for cached "negative" responses, the load time plus "negative" period


FILE HEADER RAT FIELD
---------------------
The record attributes field is used to flag whether a cached response contains
a GZIP compressed response.  If bit 1 (FAT$M_FORTRANCC, FAB$M_FTN) has been set
during cache load then this file contains a GZIP encoded response and the
client capacity to accept that cached content determines whether the file is
used or not.  Very simple, very cheap, a little inefficient, because if a
non-GZIP client accesses the cached response it will be invalidated and
reloaded.  For the cost of this approach this is acceptable, and considering
most proxy client populations will either be able to accept GZIP encoding or
not it shouldn't be a significant issue either.


CACHE INVALIDATION
------------------
For this purpose, cache invalidation is defined as the determination when a
cache file's data is no longer valid and needs to be reloaded.

The method used to for cache validation is deliberately quite simple in
algorithm and implementation. In this first attempt at a proxy server the
overriding criteria have been efficiency, simplicity of implementation, and
reliability. Wishing to avoid complicated revalidation using behind-the-scenes
HEAD requests the basic approach has been to just invalidate the cache item
upon exiry of a period related to it's "Last-Modified:" age or upon a
"no-cache" request, both described further below.

If an HTTP/1.1 "Connection: max-age=0" or HTTP/1.0 "Pragma: no-cache" request
header field is present (as is generated by many browsers when using the
"reload" function) then the server should completely reload the response from
the remote server.  (Too often the author seems to have received incomplete
responses where the proxy server caches only part of a response and has seemed
to refuse to explicitly re-request.)  OK it's a a bit more expensive but who's
to say the proxy server is right all the time! The response is still cached ...
the next request may not have the "no-cache" parameter.

When a response is cached the file creation (CDT) date/time is set to the local
equivalent of the "Last-Modified:" GMT date and time supplied with the
response.  In this manner the file's absolute age can be determined solely from
the file header.  Also, when a file is cached, the backup (BDT) and revision
(RDT) date/times are set to current.  The BDT is used when assessing when the
file was last loaded/validated/reloaded.  The RDT provides the date/time the
cache file was last accessed (read).  For "negative" responses the expires
(EDT) date/time is set to the current time plus the configuration directive
[ProxyCacheNegativeSeconds] number of seconds.  If a positve response the EDT
is set to the local equivalent of any "Expires:" supplied date/time.  In this
way both "negative" responses can naturally expire and explicitly expired
"positive" responses using the same mechanism. 

The revision count (automatically updated by VMS) tracks the absolute number of
accesses since the file was created (actually a maximum of 65535, or an
unsigned short, but that should be enough).


AGE ALGORITHM
-------------
The following configuration parameter is used to control when a file being
accessed is forceably reloaded from source.

  [ProxyCacheReloadList] ... series of comma-separated integers

This parameter supplied a series of integers representing the hours after which
an access to a cache file causes the file to be invalidated and reloaded from
it's source during the proxied request.  (Note that this parameter specifies
hours in the reverse order to [ProxyCachePurgeList].) Each number in the
series represents the lower boundary of the range between it and the next
number of hours.  A file with a last-loaded age falling within a range is
reloaded at the lower boundary of that particular range.  The following example

  [ProxyCacheReloadList] 1,2,4,8,12,24,48,96,168

would result in a file 1.5 hours old being reloaded every hour, 3.25 hours old
every 2 hours, 4-8 hours old every 4 hours, etc.  Here "old" means since last
(or of course first) loaded.  Files not reloaded since the final integer, in
this example 168, are always reloaded.


VERSION HISTORY
---------------
26-MAR-2014  MGD  ProxyCacheInit() reflect cache enbled in accounting data
08-JUN-2012  JPP  allow ResponseBufferSize to be >= 64k
02-JUN-2007  MGD  remove 204 (no content) cached negative response (WebDAV)
03-MAY-2005  MGD  "Cache-Control: max-age=0" applied only to proxy cache
26-MAR-2005  MGD  allow caching of GZIP compressed responses
14-NOV-2004  MGD  suppress caching for content-encoded (GZIPed) responses
19-SEP-2004  MGD  bugfix; even number of bytes on a disk $QIO READVBLK
04-SEP-2004  MGD  significant modifications to caching capabilities
                  some with respect to increased HTTP/1.1 compatibility
                  others to increase the finesse with what can and can't
                  be cached (see ProxCacheLoadBegin())
                  NOTE: significant changes in file header date usage!
30-APR-2004  MGD  significant changes to eliminate RMS from cache read
                  access by using ACP/QIOs (saves a few cycles, reduces
                  latency a little - RMS still abstracts the complexity
                  of creating and populating the cache files)
18-AUG-2001  MGD  prevent (pragma) reload within the specified period,
                  bugfix; a bugfix in VMS V7.2 has broken the previously
                  working usage of IO$_MODIFY in ProxyCacheSetFileDateTime()
04-AUG-2001  MGD  support module WATCHing
20-DEC-2000  MGD  routine proxy maintainence optionally disabled/external
28-AUG-2000  MGD  flat256 and 64x64 cache directory organizations
01-JUN-2000  MGD  bugfix; use 'rqHeader.RequestUriPtr' as '->RequestUriPtr'
                  (non-URL-decoded path plus query string)
03-JAN-2000  MGD  no changes required for ODS-5 compliance ... it's not
                  (see not above)
10-NOV-1999  MGD  cookie constraint relaxed (improved cache perform by 100%)
19-AUG-1998  MGD  initial development (recommenced DEC 1998)
*/
/*****************************************************************************/

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

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

/* VMS related header files */
#include <atrdef.h>
#include <descrip.h>
#include <dvidef.h>
#include <fibdef.h>
#include <iodef.h>
#include <libdef.h>
#include <libdtdef.h>
#include <psldef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application-related header files */
#include "wasd.h"

#define WASD_MODULE "PROXYCACHE"

#define RAT_GZIP 0x01   /* (FAT$M_FORTRANCC, FAB$M_FTN) */

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

#define PROXY_CACHE_DEFAULT_PURGE_LIST "168,48,24,8,0"
#define PROXY_CACHE_DEFAULT_RELOAD_LIST "1,2,4,8,12,24,48,96,168"
#define PROXY_CACHE_DIR_ORG_DEFAULT PROXY_CACHE_DIR_ORG_FLAT256

char  ErrorProxyCacheVersion [] = "Proxy cache file version mismatch!";

BOOL  ProxyCacheAllowReload = true,
      ProxyCacheEnabled,
      ProxyCacheFreeSpaceAvailable,
      ProxyReportCacheLog,
      ProxyReportLog;

int  ProxyCacheAllocationQuantityMax,
     ProxyCacheDeviceCheckMinutes,
     ProxyCacheDeviceDirOrg,
     ProxyCacheDeviceMaxPercent,
     ProxyCacheDevicePurgePercent,
     ProxyCacheDeviceTargetPercent,
     ProxyCacheFileKBytesMax,
     ProxyCacheNegativeSeconds,
     ProxyCacheNoReloadSeconds,
     ProxyCacheRoutineHourOfDay;

unsigned long  ProxyCacheNegativeSecondsDelta [2];

char  *ProxyCachePurgeListPtr,
      *ProxyCacheReloadListPtr,
      *ProxyCacheRoot;

#define PROXY_CACHE_PURGE_HOURS_MAX 32
int  ProxyCachePurgeList [PROXY_CACHE_PURGE_HOURS_MAX];
int  ProxyCachePurgeListCount;
char  ProxyCachePurgeListString [64];

#define PROXY_CACHE_RELOAD_HOURS_MAX 32
int  ProxyCacheReloadList [PROXY_CACHE_RELOAD_HOURS_MAX];
int  ProxyCacheReloadListCount;
char  ProxyCacheReloadListString [64];

char  *ProxyCache64x64 [] = {
   "0.0", "0.1", "0.2", "0.3", "1.0", "1.1", "1.2", "1.3",
   "2.0", "2.1", "2.2", "2.3", "3.0", "3.1", "3.2", "3.3",
};

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

extern int  EfnWait,
            EfnNoWait,
            ProxyReadBufferSize;

extern int  ToLowerCase[],
            ToUpperCase[];

extern unsigned long  HttpdTime64[];

extern char  ErrorSanityCheck[],
             HttpProtocol[],
             SoftwareID[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern MSG_STRUCT  Msgs;
extern PROXY_ACCOUNTING_STRUCT  *ProxyAccountingPtr;
extern WATCH_STRUCT  Watch;

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

ProxyCacheInit (BOOL CacheEnabled)

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                 "ProxyCacheInit() !&B", CacheEnabled);
 
   ProxyCacheEnabled = ProxyAccountingPtr->CacheEnabled = CacheEnabled;

   ProxyCacheRoot = v10orPrev10(PROXY_CACHE_ROOT,-1);

   if (!(ProxyCacheDeviceDirOrg = Config.cfProxy.CacheDeviceDirOrg))
      ProxyCacheDeviceDirOrg = PROXY_CACHE_DIR_ORG_DEFAULT;
   ProxyCacheDeviceCheckMinutes = Config.cfProxy.CacheDeviceCheckMinutes;
   ProxyCacheDeviceMaxPercent = Config.cfProxy.CacheDeviceMaxPercent;
   ProxyCacheDevicePurgePercent = Config.cfProxy.CacheDevicePurgePercent;
   ProxyCacheFileKBytesMax = Config.cfProxy.CacheFileKBytesMax;
   ProxyCacheNegativeSeconds = Config.cfProxy.CacheNegativeSeconds;
   ProxyCacheNoReloadSeconds = Config.cfProxy.CacheNoReloadSeconds;
   if (isdigit(Config.cfProxy.CacheRoutineHourOfDayString[0]))
      ProxyCacheRoutineHourOfDay =
         atoi(Config.cfProxy.CacheRoutineHourOfDayString);
   else
      ProxyCacheRoutineHourOfDay = -1;
   ProxyCachePurgeListPtr = Config.cfProxy.CachePurgeList;
   ProxyCacheReloadListPtr = Config.cfProxy.CachePurgeList;
   ProxyReportCacheLog = Config.cfProxy.ReportCacheLog;
   ProxyReportLog = Config.cfProxy.ReportLog;

   ProxyCacheInitValues ();

   if (ProxyCacheEnabled) ProxyMaintInit ();
}

/****************************************************************************/
/*
Make sure the configuration parameters are reasonable.  Build the purge and
reload arrays.
*/

ProxyCacheInitValues ()

{
   int  Number,
        LastNumber;
   char  *cptr;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY, "ProxyCacheInitValues()");

   if (ProxyCacheDeviceCheckMinutes < 1)
      ProxyCacheDeviceCheckMinutes = 15;
   if (ProxyCacheDeviceCheckMinutes > 60)
      ProxyCacheDeviceCheckMinutes = 60;

   if (ProxyCacheDeviceMaxPercent < 10)
      ProxyCacheDeviceMaxPercent = 85;
   if (ProxyCacheDeviceMaxPercent > 85)
      ProxyCacheDeviceMaxPercent = 85;

   if (ProxyCacheDevicePurgePercent < 1)
      ProxyCacheDevicePurgePercent = 1;
   if (ProxyCacheDevicePurgePercent > 25)
      ProxyCacheDevicePurgePercent = 25;

   ProxyCacheDeviceTargetPercent = ProxyCacheDeviceMaxPercent -
                                   ProxyCacheDevicePurgePercent;
   if (ProxyCacheDeviceTargetPercent < 0) ProxyCacheDeviceTargetPercent = 0;

   if (ProxyCacheFileKBytesMax < 1)
      ProxyCacheFileKBytesMax = 256;
   if (ProxyCacheFileKBytesMax > 1024)
      ProxyCacheFileKBytesMax = 1024;
   /* number of KBytes * 2 (2x512bytes) plus 1 block for URL, etc. */
   ProxyCacheAllocationQuantityMax = (ProxyCacheFileKBytesMax * 2) + 1;

   /* if less than zero or greater than fifty-nine minutes */
   if (ProxyCacheNegativeSeconds < 0 ||
       ProxyCacheNegativeSeconds >= 59 * 60)
      ProxyCacheNegativeSeconds = 5 * 60;
   /* create a delta time of that many seconds */
   ProxyCacheNegativeSecondsDelta[0] = -10000000 * ProxyCacheNegativeSeconds;
   ProxyCacheNegativeSecondsDelta[1] = -1;

   if (ProxyCacheRoutineHourOfDay < 0)
      ProxyCacheRoutineHourOfDay = 0;
   if (ProxyCacheRoutineHourOfDay > 24)
      ProxyCacheRoutineHourOfDay = 24;

   ProxyCacheInitPurgeList ();

   ProxyCacheInitReloadList ();
}

/****************************************************************************/
/*
Initialize the proxy maintainance purge list of hours.
*/

ProxyCacheInitPurgeList ()

{
   static $DESCRIPTOR (ListFaoDsc, "!AZ!UL");

   int  idx,
        status,
        Number,
        LastNumber;
   unsigned long  *vecptr;
   char  *cptr;
   unsigned long  FaoVector [2];
   unsigned short  Length;
   $DESCRIPTOR (ListDsc, ProxyCachePurgeListString);

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                 "ProxyCacheInitPurgeList() !&Z", ProxyCachePurgeListPtr);

   if (!ProxyCachePurgeListPtr || !ProxyCachePurgeListPtr[0])
      ProxyCachePurgeListPtr = ProxyCachePurgeListString;

   for (;;)
   {
      LastNumber = 999999999;
      ProxyCachePurgeListCount = 0;
      if (!*ProxyCachePurgeListPtr)
         ProxyCachePurgeListPtr = PROXY_CACHE_DEFAULT_PURGE_LIST;
      cptr = ProxyCachePurgeListPtr;
      while (*cptr && *ProxyCachePurgeListPtr)
      {
         while (*cptr && !isdigit(*cptr)) cptr++;
         if (!*cptr) break;
         Number = atoi(cptr);
         while (isdigit(*cptr)) cptr++;
         if (Number >= LastNumber ||
             ProxyCachePurgeListCount >= PROXY_CACHE_PURGE_HOURS_MAX)
         {
            strcpy (ProxyCachePurgeListString, "*ERROR*");
            break;
         }
         ProxyCachePurgeList[ProxyCachePurgeListCount++] = LastNumber = Number;
      }

      /* exit, having built the array of hours */
      if (*ProxyCachePurgeListPtr) break;
   }

   memset (&ProxyCachePurgeListString, 0, sizeof(ProxyCachePurgeListString));
   Length = 0;
   for (idx = 0; idx < ProxyCachePurgeListCount; idx++)
   {
      ListDsc.dsc$w_length += Length;
      ListDsc.dsc$a_pointer += Length;
      vecptr = &FaoVector;
      if (idx) *vecptr++ = ","; else *vecptr++ = "";
      *vecptr++ = ProxyCachePurgeList[idx];
      status = sys$faol (&ListFaoDsc, &Length, &ListDsc, &FaoVector);
      if (VMSnok (status))
      {
         strcpy (ProxyCachePurgeListString, "*ERROR*");
         break;
      }
   }
   ProxyCachePurgeListPtr = ProxyCachePurgeListString;

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", ProxyCachePurgeListPtr);
}

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

ProxyCacheInitReloadList ()

{
   static $DESCRIPTOR (ListFaoDsc, "!AZ!UL");

   int  idx,
        status,
        Number,
        LastNumber;
   unsigned long  FaoVector [2];
   unsigned short  Length;
   unsigned long  *vecptr;
   char  *cptr;
   $DESCRIPTOR (ListDsc, ProxyCacheReloadListString);

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                  "ProxyCacheInitReloadList() !&Z", ProxyCacheReloadListPtr);

   for (;;)
   {
      LastNumber = 0;
      ProxyCacheReloadListCount = 0;
      if (!*ProxyCacheReloadListPtr)
         ProxyCacheReloadListPtr = PROXY_CACHE_DEFAULT_RELOAD_LIST;
      cptr = ProxyCacheReloadListPtr;
      while (*cptr)
      {
         while (*cptr && !isdigit(*cptr)) cptr++;
         if (!*cptr) break;
         Number = atoi(cptr);
         while (isdigit(*cptr)) cptr++;
         if (Number <= LastNumber ||
             ProxyCacheReloadListCount >= PROXY_CACHE_RELOAD_HOURS_MAX)
         {
            ProxyCacheReloadListPtr = "";
            break;
         }
         ProxyCacheReloadList[ProxyCacheReloadListCount++] =
            LastNumber = Number;
      }

      /* exit, having built the array of hours */
      if (*ProxyCacheReloadListPtr) break;
   }

   memset (&ProxyCacheReloadListString, 0, sizeof(ProxyCacheReloadListString));
   Length = 0;
   for (idx = 0; idx < ProxyCacheReloadListCount; idx++)
   {
      ListDsc.dsc$w_length += Length;
      ListDsc.dsc$a_pointer += Length;
      vecptr = &FaoVector;
      if (idx) *vecptr++ = ","; else *vecptr++ = "";
      *vecptr++ = ProxyCacheReloadList[idx];
      status = sys$faol (&ListFaoDsc, &Length, &ListDsc, &FaoVector);
      if (VMSnok (status))
      {
         strcpy (ProxyCacheReloadListString, "*ERROR*");
         break;
      }
   }
   ProxyCacheReloadListPtr = ProxyCacheReloadListString;

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", ProxyCacheReloadListPtr);
}

/*****************************************************************************/
/*
Cleanup the RMS parse structure, ACP-QIO channel is still assigned, deaccess
any QIO-accessed read cache file and then deassign the associated channel,
finalize any cache file load.
*/

ProxyCacheEnd (PROXY_TASK *tkptr)

{
   int  status;
   PROXY_CACHE_FILE_QIO  *cfqptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY, "ProxyCacheEnd()");

   if (tkptr->ParseInUse)
   {
      /* ensure parse internal data structures are released */
      tkptr->ParseFab.fab$l_fna = "a:[b]c.d;";
      tkptr->ParseFab.fab$b_fns = 9;
      tkptr->ParseFab.fab$b_dns = tkptr->ParseNam.nam$l_rlf = 0;
      tkptr->ParseNam.nam$b_nop = NAM$M_SYNCHK;

      /* synchronous service */
      tkptr->ParseFab.fab$l_fop &= ~FAB$M_ASY;
      status = sys$parse (&tkptr->ParseFab, 0, 0);

      tkptr->ParseInUse = false;
   }

   if (tkptr->LoadFab.fab$w_ifi)
   {
      /* this function ASTs to ProxyEnd() */
      ProxyCacheLoadEnd (tkptr);
      return;
   }

   cfqptr = &tkptr->CacheFileQio;

   if (cfqptr->AcpChannel)
   {
      /* no file has been accessed, clean up after the ACP-QIO */
      sys$dassgn (cfqptr->AcpChannel);
      cfqptr->AcpChannel = 0;
   }

   if (cfqptr->QioChannel)
   {
      /* cache file has been accessed for read */
      status = sys$qiow (EfnWait, cfqptr->QioChannel, IO$_DEACCESS,
                         &cfqptr->IOsb, 0, 0,
                         &cfqptr->FibDsc, 0, 0, 0, 0, 0);
      if (VMSok (status)) status = cfqptr->IOsb.Status;
      if (VMSnok (status)) ErrorNoticed (NULL, status, NULL, FI_LI);

      sys$dassgn (cfqptr->QioChannel);
      cfqptr->QioChannel = 0;
   }

   ProxyEnd (tkptr);
}

/*****************************************************************************/
/*
Check the obvious and early criteria on whether a proxied request could be
cached.  If not just return an error status.  If it's possible to have cached
this request then construct the cache file ident block from the request
host-name[:port] and the request path.  Use this to generate the MD5 digest
string of that ident block and generate a unique cache file name using that.
Create a file FAB and NAM and attempt to parse the cache file name.
*/

int ProxyCacheReadBegin (PROXY_TASK *tkptr)

{
   BOOL  CannotCache,
         SetPathNoCache;
   int  status,
        UrlLength;
   char  *cptr, *sptr, *zptr,
         *UrlPtr;
   PROXY_CACHE_DESCR  *pcdptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY, "ProxyCacheReadBegin()");

   tkptr->ProxyCacheSuitable = tkptr->ProxyCacheFileExisted = false;

   if (!ProxyCacheEnabled) return (STS$K_ERROR);

   if (!tkptr->RequestPtr)
      SetPathNoCache = false;
   else
      SetPathNoCache = tkptr->RequestPtr->rqPathSet.NoCache;
         
   CannotCache = false;
   if (tkptr->RequestHttpMethod != HTTP_METHOD_GET)
      CannotCache = true;
   else
   if (tkptr->RequestUriQueryStringPtr[0])
      CannotCache = true;
   else
   if (tkptr->RequestHttpVersion == HTTP_VERSION_0_9)
      CannotCache = true;
   else
   if (SetPathNoCache)
      CannotCache = true;

   if (CannotCache)
   {
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         char  String [256];

         String[0] = '\0';
         if (tkptr->RequestHttpMethod != HTTP_METHOD_GET)
         {
            strcpy (String, ", method ");
            strcat (String, tkptr->RequestHttpMethodNamePtr);
         }
         if (tkptr->RequestUriQueryStringPtr[0])
            strcat (String, ", query string");
         if (tkptr->RequestHttpVersion == HTTP_VERSION_0_9)
            strcat (String, ", HTTP/0.9");
         if (SetPathNoCache)
            strcat (String, ", path set NOCACHE");
         if (!String[0]) strcpy (String, ", ?");

         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ not cachable: !AZ", String+2);
      }

      tkptr->CannotCache = true;
      InstanceGblSecIncrLong (&ProxyAccountingPtr->RequestCannotCacheCount);

      return (STS$K_ERROR);
   }

   /*************************/
   /* generate cache header */
   /*************************/

   pcdptr = (PROXY_CACHE_DESCR*)tkptr->ResponseBufferPtr;
   pcdptr->CacheVersion = PROXY_CACHE_FILE_VERSION;

   zptr = (sptr = tkptr->ResponseBufferPtr) + tkptr->ResponseBufferSize;
   sptr += sizeof(PROXY_CACHE_DESCR);
   UrlPtr = sptr;
   for (cptr = "http://";
        *cptr && sptr < zptr;
        *sptr++ = *cptr++);
   for (cptr = tkptr->RequestHostPort;
        *cptr && sptr < zptr;
        *sptr++ = *cptr++);
   /* copy in the path portion, stopping at any query string */
   for (cptr = tkptr->RequestUriPtr;
        *cptr && *cptr != '?' && sptr < zptr;
        *sptr++ = *cptr++);

   /* if cache information block overflowed then just forget it! */
   if (sptr >= zptr)
   {
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ URI overflow");
      return (STS$K_ERROR);
   }

   /* the terminating null is included in this length */
   *sptr++ = '\0';
   pcdptr->UrlLength = UrlLength = sptr - UrlPtr;

   /* now the currently free space in the buffer follows the description */
   tkptr->ResponseBufferCurrentPtr = sptr;
   tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize -
                                    (tkptr->ResponseBufferCurrentPtr -
                                     tkptr->ResponseBufferPtr);

   /* flag that this request is suitable for caching */
   tkptr->ProxyCacheSuitable = true;

   /****************************/
   /* generate cache file name */
   /****************************/

   /* generate an MD5 digest representing the "http://host-name:port/path" */
   Md5HexString (UrlPtr, UrlLength, tkptr->ProxyCacheMd5HexDigest);

   /* using the MD5 digest generate a unique cache file name */
   sptr = tkptr->ProxyCacheFileName;
   for (cptr = ProxyCacheRoot; *cptr; *sptr++ = *cptr++);
   if (ProxyCacheDeviceDirOrg == PROXY_CACHE_DIR_ORG_FLAT256)
   {
      *sptr++ = TOUP(tkptr->ProxyCacheMd5HexDigest[0]);
      *sptr++ = TOUP(tkptr->ProxyCacheMd5HexDigest[1]);
      cptr = tkptr->ProxyCacheMd5HexDigest + 2;
   }
   else
   {
      /* PROXY_CACHE_DIR_ORG_64X64 */
      *sptr++ = TOUP(tkptr->ProxyCacheMd5HexDigest[0]);
      if (tkptr->ProxyCacheMd5HexDigest[1] >= '0' &&
          tkptr->ProxyCacheMd5HexDigest[1] <= '9')
         cptr = ProxyCache64x64[tkptr->ProxyCacheMd5HexDigest[1]-'0'];
      else
         cptr = ProxyCache64x64[tkptr->ProxyCacheMd5HexDigest[1]-'a'+10];
      while (*cptr) *sptr++ = *cptr++;
      *sptr++ = TOUP(tkptr->ProxyCacheMd5HexDigest[2]);
      cptr = tkptr->ProxyCacheMd5HexDigest + 3;
   }
   *sptr++ = ']';
   while (*cptr) *sptr++ = TOUP(*cptr++);
   for (cptr = ".HTC;1"; *cptr; *sptr++ = *cptr++);
   *sptr = '\0';
   tkptr->ProxyCacheFileNameLength = sptr - tkptr->ProxyCacheFileName;

   if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                 "!AZ !AZ !AZ", UrlPtr,
                 tkptr->ProxyCacheMd5HexDigest,
                 tkptr->ProxyCacheFileName);

   /*******************/
   /* parse file name */
   /*******************/

   tkptr->ParseInUse = true;

   tkptr->ParseFab = cc$rms_fab;
   tkptr->ParseFab.fab$l_ctx = tkptr;
   tkptr->ParseFab.fab$l_fna = tkptr->ProxyCacheFileName;
   tkptr->ParseFab.fab$b_fns = tkptr->ProxyCacheFileNameLength;
   tkptr->ParseFab.fab$l_nam = &tkptr->ParseNam;

   /* initialize the NAM block */
   tkptr->ParseNam = cc$rms_nam;
   tkptr->ParseNam.nam$l_esa = tkptr->ExpandedFileName;
   tkptr->ParseNam.nam$b_ess = sizeof(tkptr->ExpandedFileName)-1;

   /* asynchronous service */
   tkptr->ParseFab.fab$l_fop |= FAB$M_ASY;
   status = sys$parse (&tkptr->ParseFab, &ProxyCacheReadAcpInfo,
                                         &ProxyCacheReadAcpInfo);

   return (SS$_NORMAL);
} 

/*****************************************************************************/
/*
Check the result of the RMS parse of the cache file name.

Get the file size and creation date/time (document last modified) of the file.

This function uses the ACP-QIO interface detailed in the "OpenVMS I/O User's 
Reference Manual", and is probably as fast as we can get for this type of file
system functionality!
*/ 

ProxyCacheReadAcpInfo (struct FAB *FabPtr)

{
   static $DESCRIPTOR (DeviceDsc, "");

   int  status;
   ATRDEF  *atptr;
   PROXY_TASK  *tkptr;
   PROXY_CACHE_FILE_QIO  *cfqptr;

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

   tkptr = FabPtr->fab$l_ctx;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheReadAcpInfo() !&F !&S !&S",
                 &ProxyCacheReadAcpInfo,
                 FabPtr->fab$l_sts, FabPtr->fab$l_stv);

   cfqptr = &tkptr->CacheFileQio;

   /* assign a channel to the disk device containing the file */
   DeviceDsc.dsc$a_pointer = tkptr->ParseNam.nam$l_dev;
   DeviceDsc.dsc$w_length = tkptr->ParseNam.nam$b_dev;

   status = sys$assign (&DeviceDsc, &cfqptr->AcpChannel, 0, 0, 0);
   if (VMSnok (status))
   {
      ProxyReadCacheFailed (tkptr);
      return;
   }

   /* set up the File Information Block for the ACP interface */
   cfqptr->FibDsc.dsc$w_length = sizeof(cfqptr->Fib);
   cfqptr->FibDsc.dsc$a_pointer = &cfqptr->Fib;

   if (VMSnok (status = tkptr->ParseFab.fab$l_sts))
   {
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ parse fail !AZ !&S",
                    tkptr->ProxyCacheFileName, status);

      if (ProxyReportCacheLog)
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file parse error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

      ProxyReadCacheFailed (tkptr);
      return;
   }

   /* copy in the parsed directory ID */
   memcpy (&cfqptr->Fib.fib$w_did, &tkptr->ParseNam.nam$w_did, 6);

   cfqptr->FileNameDsc.dsc$a_pointer = tkptr->ParseNam.nam$l_name;
   cfqptr->FileNameDsc.dsc$w_length = tkptr->ParseNam.nam$b_name +
                                      tkptr->ParseNam.nam$b_type +
                                      tkptr->ParseNam.nam$b_ver;

   atptr = &cfqptr->FileAtr;
   atptr->atr$w_size = sizeof(cfqptr->BdtTime64);
   atptr->atr$w_type = ATR$C_BAKDATE;
   atptr->atr$l_addr = &cfqptr->BdtTime64;
   atptr++;
   atptr->atr$w_size = sizeof(cfqptr->CdtTime64);
   atptr->atr$w_type = ATR$C_CREDATE;
   atptr->atr$l_addr = &cfqptr->CdtTime64;
   atptr++;
   atptr->atr$w_size = sizeof(cfqptr->EdtTime64);
   atptr->atr$w_type = ATR$C_EXPDATE;
   atptr->atr$l_addr = &cfqptr->EdtTime64;
   atptr++;
   atptr->atr$w_size = sizeof(cfqptr->RdtTime64);
   atptr->atr$w_type = ATR$C_REVDATE;
   atptr->atr$l_addr = &cfqptr->RdtTime64;
   atptr++;
   atptr->atr$w_size = sizeof(cfqptr->AscDates);
   atptr->atr$w_type = ATR$C_ASCDATES;
   atptr->atr$l_addr = &cfqptr->AscDates;
   atptr++;
   atptr->atr$w_size = sizeof(cfqptr->RecAttr);
   atptr->atr$w_type = ATR$C_RECATTR;
   atptr->atr$l_addr = &cfqptr->RecAttr;
   atptr++;
   atptr->atr$w_size = atptr->atr$w_type = atptr->atr$l_addr = 0;

   status = sys$qio (EfnNoWait, cfqptr->AcpChannel, IO$_ACCESS,
                     &cfqptr->IOsb, &ProxyCacheReadAcpInfoAst, tkptr, 
                     &cfqptr->FibDsc, &cfqptr->FileNameDsc, 0, 0,
                     &cfqptr->FileAtr, 0);
   if (VMSnok (status))
   {
      /* let the AST routine handle it! */
      cfqptr->IOsb.Status = status;
      SysDclAst (&ProxyCacheReadAcpInfoAst, tkptr);
   }
}

/****************************************************************************/
/*
The ACP-QIO has concluded delivering this AST routine.
*/

ProxyCacheReadAcpInfoAst (PROXY_TASK *tkptr)

{
   int  status,
        ExpiresHours,
        ExpiresSeconds,
        LastAccessHours,
        LastAccessSeconds,
        LastLoadHours,
        LastLoadSeconds,
        LastModifiedHours,
        LastModifiedSeconds;
   unsigned long  ResultTime64 [2];
   PROXY_CACHE_FILE_QIO  *cfqptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheReadAcpInfoAst() !&F !&S",
                 &ProxyCacheReadAcpInfoAst, tkptr->CacheFileQio.IOsb.Status);

   rqptr = tkptr->RequestPtr;

   /* get the file ACP pointer from the task structure */
   cfqptr = &tkptr->CacheFileQio;

   if (VMSnok (status = cfqptr->IOsb.Status))
   {
      /*************/
      /* ACP error */
      /*************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         if (status == SS$_NOSUCHFILE)
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ not found !AZ",
                       tkptr->ProxyCacheFileName);
         else
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE ACP fail !AZ !&S",
                       tkptr->ProxyCacheFileName, status);
      }

      if (ProxyReportCacheLog)
         if (status != SS$_NOSUCHFILE)
            FaoToStdout (
"%HTTPD-W-PROXY, !20%D, file ACP error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
               0, FI_LI, status, tkptr->ProxyCacheFileName);

      ProxyReadCacheFailed (tkptr);
      return;
   }

   /* note that the cache file at least existed at the time it was checked! */
   tkptr->ProxyCacheFileExisted = true;

   /* record attributes used to indicate the content is GZIP compressed */
   if (cfqptr->RecAttr.fat$b_rattrib & RAT_GZIP)
   {
      /* cache file content is GZIP encoded */
      if (!rqptr->rqHeader.AcceptEncodingGzip)
      {
         /* client does not support that */
         ProxyReadCacheFailed (tkptr);
         return;
      }
      rqptr->rqResponse.ContentIsEncodedGzip = true;
   }

   cfqptr->EndOfFileVbn = ((cfqptr->RecAttr.fat$l_efblk & 0xffff) << 16) |
                          ((cfqptr->RecAttr.fat$l_efblk & 0xffff0000) >> 16);

   cfqptr->FirstFreeByte = cfqptr->RecAttr.fat$w_ffbyte;

   if (cfqptr->EndOfFileVbn <= 1)
      cfqptr->SizeInBytes = cfqptr->RecAttr.fat$w_ffbyte;
   else
      cfqptr->SizeInBytes = ((cfqptr->EndOfFileVbn-1) << 9) +
                            cfqptr->RecAttr.fat$w_ffbyte;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "EndOfFileVbn:!UL FirstFreeByte:!UL SizeInBytes:!UL",
                  cfqptr->EndOfFileVbn, cfqptr->FirstFreeByte,
                  cfqptr->SizeInBytes);

   if (!cfqptr->SizeInBytes)
   {
      /* file header has just created during cache load - no content yet */
      ProxyReadCacheFailed (tkptr);
      return;
   }

   LastLoadSeconds = ProxyCacheDeltaSeconds (&HttpdTime64,
                                             &cfqptr->BdtTime64);
   LastLoadHours = LastLoadSeconds / 3600;
   LastModifiedSeconds = ProxyCacheDeltaSeconds (&HttpdTime64,
                                                 &cfqptr->CdtTime64);
   LastModifiedHours = LastModifiedSeconds / 3600;

   if (WATCHING (tkptr, WATCH_PROXY_CACHE))
   {
      LastAccessSeconds = ProxyCacheDeltaSeconds (&HttpdTime64,
                                                  &cfqptr->RdtTime64);
      LastAccessHours = LastAccessSeconds / 3600;
      if (QUAD_ZERO(cfqptr->EdtTime64))
         ExpiresHours = ExpiresSeconds = 0;
      else
      {
         ExpiresSeconds = ProxyCacheDeltaSeconds (&cfqptr->EdtTime64,
                                                  &HttpdTime64);
         ExpiresHours = ExpiresSeconds / 3600;
      }

      WatchThis (tkptr->RequestPtr, WATCH_PROXY_CACHE,
"CACHE READ gzip:!AZ noreload:!UL/!UL negative:!SL/!UL modified:!UL(!UL) \
loaded:!UL(!UL) expires:!UL(!UL) accessed:!UL(!UL) (!UL time!%s) !AZ",
         (cfqptr->RecAttr.fat$b_rattrib & RAT_GZIP) ? "yes" : "no",
         LastLoadSeconds, ProxyCacheNoReloadSeconds,
         ExpiresSeconds, ProxyCacheNegativeSeconds,
         LastModifiedHours, LastModifiedHours/24,
         LastLoadHours, LastLoadHours/24,
         ExpiresHours, ExpiresHours/24,
         LastAccessHours, LastAccessHours/24,
         cfqptr->AscDates.RevisionCount,
         tkptr->ProxyCacheFileName);
   }

   if (QUAD_ZERO(cfqptr->BdtTime64))
   {
      /********************/
      /* from v6.0 cache! */
      /********************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ v6.0 RELOAD");

      ProxyReadCacheFailed (tkptr);
      return;
   }

   /******************/
   /* check currency */
   /******************/

   if (ProxyCacheAllowReload &&
       LastLoadSeconds >= ProxyCacheNoReloadSeconds)
   {
      if (tkptr->RequestNotFromCache)
      {
         /******************/
         /* not-from-cache */
         /******************/

         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ not-from-cache RELOAD");

         ProxyReadCacheFailed (tkptr);
         return;
      }

      if (rqptr->rqHeader.CacheControlMaxAge &&
          (LastLoadSeconds > rqptr->rqHeader.CacheControlMaxAge ||
           LastModifiedSeconds > rqptr->rqHeader.CacheControlMaxAge))
      {
         /***********/
         /* max-age */
         /***********/

         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ max-age RELOAD");

         ProxyReadCacheFailed (tkptr);
         return;
      }
   }

   if (QUAD_NOT_ZERO(cfqptr->EdtTime64))
   {
      /************/
      /* expired? */
      /************/

      status = lib$sub_times (&cfqptr->EdtTime64,
                              &HttpdTime64,
                              &ResultTime64);
      if (VMSnok(status))
      {
         /* status will be LIB$_NEGTIM if expired */
         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ expired RELOAD");

         ProxyReadCacheFailed (tkptr);
         return;
      }
   }

   if (ProxyCacheReloadLastModified (LastModifiedHours, LastLoadHours))
   {
      /***********************/
      /* last-modified limit */
      /***********************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ last-modified limit RELOAD");

      ProxyReadCacheFailed (tkptr);
      return;
   }

   /* now it's a read! */
   InstanceGblSecIncrLong (&ProxyAccountingPtr->CacheReadCount);

   if (QUAD_NOT_ZERO(rqptr->rqTime.IfModifiedSinceTime64) &&
       !tkptr->RequestNotFromCache)
   {
      /*********************/
      /* if modified since */
      /*********************/

      DICT_ENTRY_STRUCT  *denptr;

      status = HttpIfModifiedSince (rqptr, &cfqptr->CdtTime64, -1);
      if (VMSnok (status))
      {
         /* status will be LIB$_NEGTIM if not modified/sent */
         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ not modified");

         /* update the last-accessed (RDT) time */
         ProxyCacheSetFileDateTime (&tkptr->CacheFileQio,
                                    &cfqptr->BdtTime64,
                                    &cfqptr->CdtTime64,
                                    &cfqptr->EdtTime64,
                                    &HttpdTime64);

         tkptr->ResponseStatusCode = 304;
         InstanceGblSecIncrLong (&ProxyAccountingPtr->CacheRead304Count);

         /* the 304 header length */
         if (denptr = ResponseDictHeader (rqptr))
            tkptr->ProxyCacheReadBytes = DICT_GET_VALUE_LEN(denptr);
         else
            tkptr->ProxyCacheReadBytes = 0;

         ProxyEnd (tkptr);
         return;
      }
   }

   /*******************/
   /* 'open' the file */
   /*******************/

   cfqptr->Fib.fib$l_acctl = FIB$M_SEQONLY | FIB$M_NOWRITE;
   cfqptr->Fib.fib$w_nmctl = FIB$M_FINDFID;

   status = sys$qio (EfnNoWait, cfqptr->AcpChannel,
                     IO$_ACCESS | IO$M_ACCESS,
                     &cfqptr->IOsb, &ProxyCacheReadAccessAst, tkptr,
                     &cfqptr->FibDsc, 0, 0, 0, 0, 0);
   if (VMSnok (status))
   {
      /* let the AST routine handle it! */
      cfqptr->IOsb.Status = status;
      SysDclAst (ProxyCacheReadAccessAst, tkptr);
   }
}

/*****************************************************************************/
/*
The file access $QIO (the 'open') has completed.  Check status.
*/

ProxyCacheReadAccessAst (PROXY_TASK *tkptr)

{
   int  status;
   PROXY_CACHE_FILE_QIO  *cfqptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheReadAccessAst() !&F !&S",
                 &ProxyCacheReadAccessAst, tkptr->CacheFileQio.IOsb.Status);

   cfqptr = &tkptr->CacheFileQio;

   if (VMSnok (status = cfqptr->IOsb.Status))
   {
      /****************/
      /* 'open' error */
      /****************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ access fail !AZ !&S",
                    tkptr->ProxyCacheFileName, status);

      if (ProxyReportCacheLog)
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file open error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

      ProxyReadCacheFailed (tkptr);
      return;
   }

   /***********/
   /* success */
   /***********/

   /* now a file is being accessed on the original ACP-QIO channel */
   cfqptr->QioChannel = cfqptr->AcpChannel;
   cfqptr->AcpChannel = 0;

   tkptr->ProxyCacheReadBytes = 0;

   /* fudge the first network write status */
   tkptr->RequestPtr->NetIoPtr->WriteStatus = SS$_NORMAL;

   ProxyCacheReadNext (tkptr->RequestPtr);
}

/*****************************************************************************/
/*
************
*** NOTE ***  This function takes a pointer to a request!!!
************

Queue a read of the next series of Virtual Blocks from the file.  When the 
read completes call ProxyCacheNextAST() function to send the data to the
client.  Don't bother to test any status here, the AST routine will do that!
*/ 

ProxyCacheReadNext (REQUEST_STRUCT *rqptr)

{
   int  status,
        BodyLength,
        BufferSize,
        DataLength;
   char  *BodyPtr,
         *DataPtr;
   PROXY_CACHE_DESCR  *pcdptr;
   PROXY_CACHE_FILE_QIO  *cfqptr;
   PROXY_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_PROXY, "ProxyCacheReadNext()");

   tkptr = rqptr->ProxyTaskPtr; 

   if (VMSnok (status = rqptr->NetIoPtr->WriteStatus))
   {
      /******************************/
      /* client network write error */
      /******************************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ client write fail !&S", status);

      ProxyCacheEnd (tkptr);
      return;
   }

   /*********************/
   /* first/next blocks */
   /*********************/

   cfqptr = &tkptr->CacheFileQio;

   if (cfqptr->BlockNumber)
   {
      if (!tkptr->ResponseHeaderSent)
      {
         tkptr->ResponseHeaderSent = true;

         DataPtr = cfqptr->BufferPtr;
         DataLength = cfqptr->BufferCount;
         pcdptr = (PROXY_CACHE_DESCR*)DataPtr;

         BodyPtr = DataPtr + sizeof(PROXY_CACHE_DESCR) +
                   pcdptr->UrlLength + pcdptr->HeaderLength;
         BodyLength = DataLength - sizeof(PROXY_CACHE_DESCR) -
                      pcdptr->UrlLength - pcdptr->HeaderLength;

         if (BodyLength)
         {
            tkptr->ProxyCacheReadBytes += BodyLength;
            NetWrite (rqptr, &ProxyCacheReadNext, BodyPtr, BodyLength);
            return;
         }
      }

      if (cfqptr->EndOfFile)
      {
         cfqptr->EndOfFile = false;
         cfqptr->IOsb.Status = SS$_ENDOFFILE;
         ProxyCacheReadNextAst (tkptr);
         return;
      }

      /* get next lot of virtual blocks */
      cfqptr->BlockNumber += cfqptr->BufferSize >> 9;
   }
   else
   {
      cfqptr->BlockNumber = 1;
      cfqptr->BufferPtr = tkptr->ResponseBufferPtr;
      /* make buffer size an even number of 512 byte blocks */
      cfqptr->BufferSize = tkptr->ResponseBufferSize & 0x7ffffe00;
      tkptr->ResponseBufferSize = cfqptr->BufferSize;
   }

   BufferSize = cfqptr->BufferSize;
   if ((cfqptr->BlockNumber-1) * 512 + BufferSize >= cfqptr->SizeInBytes)
   {
      BufferSize = cfqptr->SizeInBytes - (cfqptr->BlockNumber-1) * 512;
      /* it's also a calculated EOF! */
      cfqptr->EndOfFile = true;
   }

   if (WATCHMOD (rqptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_PROXY, "vbn:!UL buf:!UL size:!UL",
                 cfqptr->BlockNumber, cfqptr->BufferSize, BufferSize);

   /*
      Documented in the VMS I/O Users Guide section entitled "Disk Function
      Codes" ... "P2--The number of bytes that are to be read from the disk,
      or written from memory to the disk. An even number must be specified if
      the controller is an RK611, RL11, RX211, or UDA50".
      Well, make sure we ask for an even number of bytes!
      Thanks to Dave Holland for demonstrating this bug on SIMH, and to
      Mark Pizzolato from the SIMH mailing list for pointing out the above.
   */
   if (BufferSize & 1)
   {
      BufferSize++;
      cfqptr->AdjustBuffer = 1;
   }
   else
      cfqptr->AdjustBuffer = 0;

   status = sys$qio (EfnNoWait, cfqptr->QioChannel,
                     IO$_READVBLK, &cfqptr->IOsb,
                     &ProxyCacheReadNextAst, tkptr,
                     cfqptr->BufferPtr, BufferSize, cfqptr->BlockNumber,
                     0, 0, 0);
   if (VMSnok (status))
   {
      /* let the AST routine handle it! */
      cfqptr->IOsb.Status = status;
      SysDclAst (&ProxyCacheReadNextAst, tkptr);
   }
}

/*****************************************************************************/
/*
The read of the next series of Virtual Blocks from the file has completed.  
Post-process and/or queue a network write to the client.  When the network 
write completes it will call the function ProxyCacheReadNextAst() to queue a
read of the next series of blocks.
*/ 

ProxyCacheReadNextAst (PROXY_TASK *tkptr)

{
   int  status,
        BodyLength,
        DataLength;
   char  *BodyPtr,
         *DataPtr,
         *HeaderPtr,
         *UrlPtr;
   REQUEST_STRUCT  *rqptr;
   PROXY_CACHE_DESCR  *pcdptr;
   PROXY_CACHE_FILE_QIO  *cfqptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheReadNextAst() !&F !&S",
                 &ProxyCacheReadNextAst, tkptr->CacheFileQio.IOsb.Status);

   rqptr = tkptr->RequestPtr;
   cfqptr = &tkptr->CacheFileQio;

   if (VMSnok (status = cfqptr->IOsb.Status))
   {
      /*************************/
      /* cache file read error */
      /*************************/

      if (status == SS$_ENDOFFILE)
      {
         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ eof !UL bytes",
                       tkptr->ProxyCacheReadBytes -
                       tkptr->RebuiltResponseLength);

         /* update the last-accessed (RDT) time */
         ProxyCacheSetFileDateTime (&tkptr->CacheFileQio,
                                    &cfqptr->BdtTime64,
                                    &cfqptr->CdtTime64,
                                    &cfqptr->EdtTime64,
                                    &HttpdTime64);

         ProxyCacheEnd (tkptr);
         return;
      }

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE READ fail !&S", status);

      rqptr->rqResponse.HttpStatus = 500;
      rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
      ErrorVmsStatus (rqptr, status, FI_LI);
      ProxyCacheEnd (tkptr);
      return;
   }

   /* get the count from the I/O status block (adjusted as necessary) */
   cfqptr->IOsb.Count -= cfqptr->AdjustBuffer;
   cfqptr->BufferCount = cfqptr->IOsb.Count;

   DataPtr = cfqptr->BufferPtr;
   DataLength = cfqptr->BufferCount;

   if (cfqptr->BlockNumber == 1)
   {
      /****************/
      /* initial read */
      /****************/

      if (!cfqptr->BufferCount)
      {
         /* in between the ACP-QIO and opening it has started reloading */
         ErrorNoticed (rqptr, SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
         /* just continue on, closing the cache file in ProxyCacheEnd() */
         ProxyReadCacheFailed (tkptr);
         return;
      }

      /* contains cache file description (lengths and request URL) */
      pcdptr = (PROXY_CACHE_DESCR*)DataPtr;

      if (pcdptr->CacheVersion != PROXY_CACHE_FILE_VERSION)
      {
         /***************/
         /* delete file */
         /***************/

         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ cache VERSION MISMATCH");

         /* this will happen very infrequently - never I should imagine! */
         status = sys$qiow (EfnWait, cfqptr->QioChannel, 
                            IO$_DELETE | IO$M_DELETE,
                            &cfqptr->IOsb, 0, 0, 
                            &cfqptr->FibDsc, 0, 0, 0, 0, 0);
         if (VMSok (status)) status = cfqptr->IOsb.Status;
         if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);

         rqptr->rqResponse.HttpStatus = 500;
         ErrorGeneral (rqptr, ErrorProxyCacheVersion, FI_LI);
         ProxyCacheEnd (tkptr);
         return;
      }

      /******************/
      /* rebuild header */
      /******************/

      /* cache file description components */
      UrlPtr = DataPtr + sizeof(PROXY_CACHE_DESCR);
      HeaderPtr = UrlPtr + pcdptr->UrlLength;

      status = ProxyResponseRebuild (tkptr, HeaderPtr, pcdptr->HeaderLength);
      if (VMSnok (status))
      {
         ProxyReadCacheFailed (tkptr);
         return;
      }

      if (tkptr->RequestHttpVersion == HTTP_VERSION_1_0 &&
          tkptr->ResponseHttpVersion == HTTP_VERSION_1_1 &&
          tkptr->ResponseTransferEncodingChunked)
      {
         /* probably won't understand it */
         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE READ downgrade HTTP protocol RELOAD");
         ProxyReadCacheFailed (tkptr);
         return;
      }

      if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      {
         BodyPtr = HeaderPtr + pcdptr->HeaderLength;
         BodyLength = DataLength - sizeof(PROXY_CACHE_DESCR) -
                      pcdptr->UrlLength - pcdptr->HeaderLength;
         WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                    "ver:!XL url:!UL hdr:!UL http:!UL len:!SL bod:!UL eof:!&B",
                    pcdptr->CacheVersion, pcdptr->UrlLength,
                    pcdptr->HeaderLength, tkptr->ResponseStatusCode,
                    tkptr->ResponseContentLength, BodyLength,
                    cfqptr->EndOfFile);
         WatchDataFormatted ("{!UL}!-!#AZ\n{!UL}!-!#AZ\n{!UL}!-!#AZ\n",
                             pcdptr->UrlLength, UrlPtr,
                             pcdptr->HeaderLength, HeaderPtr,
                             tkptr->RebuiltResponseLength,
                             tkptr->RebuiltResponsePtr);
      }

      DataPtr = tkptr->RebuiltResponsePtr;
      DataLength = tkptr->RebuiltResponseLength;
   }

   tkptr->ProxyCacheReadBytes += DataLength;

   NetWrite (rqptr, &ProxyCacheReadNext, DataPtr, DataLength);
}

/*****************************************************************************/
/*
Begin to load a file to cache the contents of a proxied request.  First assess
whether the request meets the criteria for caching.  If it does not. If it does
then initialize the RMS structures for the file and call the create service,
which ASTs to ProxyCacheLoadConnect().  Check the success there.
*/

int ProxyCacheLoadBegin (PROXY_TASK *tkptr)

{
   static unsigned short  EmptyDid [3] = { 0, 0, 0 };

   BOOL  CannotCache,
         CacheNegative,
         CachePositive;
   int  status,
        ExpiresHours,
        ExpiresSeconds,
        LastModifiedHours,
        LastModifiedSeconds,
        MaxAge;
   unsigned int  AllocationQuantity;
   PROXY_CACHE_DESCR  *pcdptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheLoadBegin()");

   rqptr = tkptr->RequestPtr;

   if (!ProxyCacheFreeSpaceAvailable)
   {
      /* no file system space available for caching purposes */
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE LOAD insufficient device space (!UL% max)",
                    ProxyCacheDeviceMaxPercent);
         ProxyMaintWatchDeviceStats ();
      }
      return (STS$K_ERROR);
   }

   if (QUAD_ZERO(tkptr->ResponseLastModified))
      tkptr->ProxyCacheLastModifiedHours = LastModifiedHours =
         LastModifiedSeconds = 0;
   else
   {
      LastModifiedSeconds =
         ProxyCacheDeltaSeconds (&HttpdTime64,
                                 &tkptr->ResponseLastModifiedTime64);
      tkptr->ProxyCacheLastModifiedHours = LastModifiedHours =
         LastModifiedSeconds / 3600;
   }

   /* 's-maxage' takes precedence over 'max-age', and over any 'expires' */
   if ((MaxAge = tkptr->ResponseCacheControlSMaxAge) ||
       (MaxAge = tkptr->ResponseCacheControlMaxAge))
   {
      static long  AddendZero = 0,
                   OneSecondDelta = -10000000;
      unsigned long  DeltaTime64 [2];
      /* ensure it doesn't appear negative, still plenty of range left */
      MaxAge &= 0x7fffffff;
      status = lib$emul (&MaxAge, &OneSecondDelta, &AddendZero, &DeltaTime64);
      if (VMSnok (status))
      {
         ErrorNoticed (rqptr, status, NULL, FI_LI);
         PUT_ZERO_QUAD (DeltaTime64);
      }
      else
      {
         status = lib$add_times (&HttpdTime64, &DeltaTime64,
                                 &tkptr->ResponseExpiresTime64);
         if (VMSnok (status))
         {
            ErrorNoticed (rqptr, status, NULL, FI_LI);
            PUT_ZERO_QUAD (tkptr->ResponseExpiresTime64);
         }
      }
      if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
         WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                    "!UL !%D", MaxAge, &tkptr->ResponseExpiresTime64);
   }

   if (QUAD_ZERO(tkptr->ResponseExpiresTime64))
      ExpiresHours = ExpiresSeconds = 0;
   else
   {
      ExpiresSeconds = ProxyCacheDeltaSeconds (&tkptr->ResponseExpiresTime64,
                                               &HttpdTime64);
      ExpiresHours = ExpiresSeconds / 3600;
   }

   /**************/
   /* cacheable? */
   /**************/

   CacheNegative = CachePositive = false;

   if (tkptr->ResponseStatusCode == 200 ||
       tkptr->ResponseStatusCode == 203 ||
       tkptr->ResponseStatusCode == 300 ||
       tkptr->ResponseStatusCode == 301 ||
       tkptr->ResponseStatusCode == 410)
   {
      /* can cache this success response */
      CachePositive = true; 
   }
   else
   if (ProxyCacheNegativeSeconds &&
       (tkptr->ResponseStatusCode == 305 ||
        tkptr->ResponseStatusCode == 400 ||
        tkptr->ResponseStatusCode == 403 ||
        tkptr->ResponseStatusCode == 404 ||
        tkptr->ResponseStatusCode == 405 ||
        tkptr->ResponseStatusCode == 414 ||
        tkptr->ResponseStatusCode == 500 ||
        tkptr->ResponseStatusCode == 502 ||
        tkptr->ResponseStatusCode == 503 ||
        tkptr->ResponseStatusCode == 504))
   {
      /* can cache this non-success (negative) response */
      CacheNegative = true;

      if (QUAD_ZERO(tkptr->ResponseExpiresTime64))
      {
         /* negative response did not explicitly specify an expiry/max-age */
         status = lib$add_times (&HttpdTime64,
                                 &ProxyCacheNegativeSecondsDelta,
                                 &tkptr->ResponseExpiresTime64);
         if (VMSnok (status))
         {
            ErrorNoticed (rqptr, status, NULL, FI_LI);
            PUT_ZERO_QUAD (tkptr->ResponseExpiresTime64);
         }
      }
   }

   if (CachePositive || CacheNegative)
   {
      /* slightly Japanese but never mind */
      CannotCache = false;
      if (tkptr->ResponseNoCache)
         CannotCache = true;
      else
      if (CachePositive && LastModifiedHours < 1)
         CannotCache = true;
      else
      if (CachePositive &&
          QUAD_NOT_ZERO(tkptr->ResponseExpires) && ExpiresHours < 1)
         CannotCache = true;
      else
      if (tkptr->ResponseCacheControlMaxAgeZero)
         CannotCache = true;
      else
      if (tkptr->ResponseContentEncodingUnknown)
         CannotCache = true;
      else
      if (tkptr->ResponseContentLength >= 0 &&
          (tkptr->ResponseContentLength >> 10) + 1 >
          ProxyCacheFileKBytesMax)
         CannotCache = true;
      else
      if (tkptr->RequestAuthorization &&
          !tkptr->ResponseCacheControlPublic)
         CannotCache = true;
      else
      if (tkptr->ResponseVaryUnsupported)
         CannotCache = true;
      else
      if (tkptr->ResponseContentTypeMultipart)
         CannotCache = true;
      else
      if (tkptr->ResponseHttpVersion == HTTP_VERSION_0_9)
         CannotCache = true;
   }
   else
      CannotCache = true;

   if (CannotCache)
   {
      /******************/
      /* not cache-able */
      /******************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         char  String [256];

         String[0] = '\0';
         if (!(CachePositive || CacheNegative))
            sprintf (String, ", status %d", tkptr->ResponseStatusCode);
         if (tkptr->ResponseNoCache)
            strcat (String, ", no-cache");
         if (CachePositive)
         {
            if (QUAD_ZERO(tkptr->ResponseLastModified))
               strcat (String, ", no last-modified");
            else
            if (LastModifiedHours < 1)
               strcat (String, ", last-modified < 1hr");
            if (QUAD_NOT_ZERO(tkptr->ResponseExpires) && ExpiresHours < 1)
               strcat (String, ", expires < 1hr");
         }
         if (tkptr->ResponseCacheControlMaxAgeZero)
            strcat (String, ", max-age=0");
         if (tkptr->ResponseContentEncodingUnknown)
            strcat (String, ", content-encoding");
         if (tkptr->ResponseContentLength >= 0 &&
             (tkptr->ResponseContentLength >> 10) + 1 >
             ProxyCacheFileKBytesMax)
         {
            char  *cptr;
            for (cptr = String; *cptr; cptr++);
            sprintf (cptr, ", content-length %d > %d kB",
                     (tkptr->ResponseContentLength >> 10) + 1,
                     ProxyCacheFileKBytesMax);
         }
         if (tkptr->RequestAuthorization &&
             !tkptr->ResponseCacheControlPublic)
            strcat (String, ", request authorization");
         if (tkptr->ResponseVaryUnsupported)
            strcat (String, ", unsupported vary");
         if (tkptr->ResponseContentTypeMultipart)
            strcat (String, ", multipart");
         if (tkptr->ResponseHttpVersion == HTTP_VERSION_0_9)
            strcat (String, ", HTTP/0.9");
         if (!String[0]) strcpy (String, ", ?");

         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE LOAD not cachable: !AZ", String + 2);
      }

      /* 'tkptr->ProxyCacheSuitable' checked method and query string */
      if (tkptr->ResponseStatusCode == 304)
      {
         if (tkptr->ProxyCacheFileExisted)
         {
            /*****************************/
            /* not modified, leave as-is */
            /*****************************/

            if (WATCHING (tkptr, WATCH_PROXY_CACHE))
               WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                          "CACHE LOAD not modified !AZ",
                          tkptr->ProxyCacheFileName);

            /* update loaded (BDT) and last accessed (RDT) times to current */
            ProxyCacheSetFileDateTime (&tkptr->CacheFileQio,
                                       &HttpdTime64,
                                       &tkptr->CacheFileQio.CdtTime64,
                                       &tkptr->CacheFileQio.EdtTime64,
                                       &HttpdTime64);

            return (STS$K_ERROR);
         }
      }

      /* it's not cacheable but already existed so delete it! */
      if (tkptr->ProxyCacheFileExisted)
      {
         /******************************/
         /* delete original cache file */
         /******************************/

         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE LOAD delete !AZ", tkptr->ProxyCacheFileName);

         tkptr->ParseFab.fab$l_fop |= FAB$M_DLT;
         /* synchronous service */
         tkptr->ParseFab.fab$l_fop &= ~FAB$M_ASY;
         status = sys$erase (&tkptr->ParseFab, 0, 0);

         if (ProxyReportCacheLog)
            if (VMSnok (status))
               FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file erase error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
                  0, FI_LI, status, tkptr->ProxyCacheFileName);
      }

      tkptr->CannotCache = true;
      InstanceGblSecIncrLong (&ProxyAccountingPtr->ResponseCannotCacheCount);

      return (STS$K_ERROR);
   }

   if (MATCH6 (&tkptr->ParseNam.nam$w_did, &EmptyDid))
   {
      /**********************************/
      /* cache directory does not exist */
      /**********************************/

      status = ProxyCacheCreateDirectory (tkptr);

      /* can't create the file if there's no directory to create it in! */
      if (VMSnok (status)) return (STS$K_ERROR);
   }

   /**************/
   /* begin load */
   /**************/

   InstanceGblSecIncrLong (&ProxyAccountingPtr->CacheWriteCount);

   /* slip the header length into the cache file description block */
   pcdptr = (PROXY_CACHE_DESCR*)tkptr->ResponseBufferPtr;
   pcdptr->HeaderLength = tkptr->ResponseHeaderLength;

   AllocationQuantity = sizeof(PROXY_CACHE_DESCR) +
                        pcdptr->UrlLength +
                        tkptr->ResponseHeaderLength;
   if (tkptr->ResponseContentLength > 0)
      AllocationQuantity += tkptr->ResponseContentLength;
   AllocationQuantity = (AllocationQuantity >> 9) + 1;
   /* bit of a sanity check ... don't want to chew up the whole disk */
   if (AllocationQuantity > ProxyCacheAllocationQuantityMax)
      AllocationQuantity = ProxyCacheAllocationQuantityMax;

   /* if there was no last-modified supplied with the (negative) response */
   if (QUAD_ZERO(tkptr->ResponseLastModifiedTime64))
      PUT_QUAD_QUAD (HttpdTime64, tkptr->ResponseLastModifiedTime64);

   if (WATCHING (tkptr, WATCH_PROXY_CACHE))
   {
      if (tkptr->ResponseContentLength >= 0)
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE !&?RE\r\rLOAD !AZ content !UL bytes (!UL blocks)",
                    tkptr->ProxyCacheFileExisted,
                    tkptr->ProxyCacheFileName,
                    tkptr->ResponseContentLength,
                    AllocationQuantity);
      else
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE !&?RE\r\rLOAD !AZ content ? bytes (!UL blocks)",
                    tkptr->ProxyCacheFileExisted,
                    tkptr->ProxyCacheFileName,
                    AllocationQuantity);
   }

   tkptr->LoadFab = cc$rms_fab;
   tkptr->LoadFab.fab$l_alq = AllocationQuantity;
   tkptr->LoadFab.fab$l_ctx = tkptr;
   tkptr->LoadFab.fab$b_fac = FAB$M_PUT | FAB$M_TRN;
   tkptr->LoadFab.fab$l_fna = tkptr->ProxyCacheFileName;
   tkptr->LoadFab.fab$b_fns = tkptr->ProxyCacheFileNameLength;
   tkptr->LoadFab.fab$l_fop = FAB$M_CIF | FAB$M_SQO |
                              FAB$M_DFW | FAB$M_TEF;
   tkptr->LoadFab.fab$l_nam = &tkptr->ParseNam;

   /* record attributes used to indicate the content is GZIP compressed */
   if (tkptr->ResponseContentEncodingGzip)
      tkptr->LoadFab.fab$b_rat = RAT_GZIP;
   else
      tkptr->LoadFab.fab$b_rat = 0;

   tkptr->LoadFab.fab$b_rfm = FAB$C_UDF;

   tkptr->LoadFab.fab$b_shr = FAB$M_NIL;
   tkptr->LoadFab.fab$l_xab = &tkptr->LoadXabDat;

   /* initialize the name block */
   tkptr->ParseNam = cc$rms_nam;
   tkptr->ParseNam.nam$l_esa = tkptr->ExpandedFileName;
   tkptr->ParseNam.nam$b_ess = sizeof(tkptr->ExpandedFileName)-1;

   /* date/time extended attribute block */
   tkptr->LoadXabDat = cc$rms_xabdat;
   tkptr->LoadXabDat.xab$l_nxt = &tkptr->LoadXabPro;
   /* last loaded (BDT) */
   PUT_QUAD_QUAD (HttpdTime64, &tkptr->LoadXabDat.xab$q_bdt);
   /* last modified (CDT) */
   PUT_QUAD_QUAD (tkptr->ResponseLastModifiedTime64,
                  &tkptr->LoadXabDat.xab$q_cdt);
   /* expires at (EDT) */
   PUT_QUAD_QUAD (tkptr->ResponseExpiresTime64,
                  &tkptr->LoadXabDat.xab$q_edt);
   /* last accessed (RDT) will be set at $CLOSE */

   /* protection extended attribute block */
   tkptr->LoadXabPro = cc$rms_xabpro;
   /* W:RE,G:RE,O:RWED,S:RWED */
   tkptr->LoadXabPro.xab$w_pro = 0xaa00;

   /* asynchronous service */
   tkptr->LoadFab.fab$l_fop |= FAB$M_ASY;
   status = sys$create (&tkptr->LoadFab, ProxyCacheLoadConnect,
                                         ProxyCacheLoadConnect);

   return (SS$_NORMAL);
} 

/*****************************************************************************/
/*
AST function delivered from ProxyCacheLoadBegin(). Check the success of the
file create. If not report it to the process log and forget about caching the
file. If OK initialize a RAB structure and call the connect service, which
ASTs to ProxyCacheLoadConnectAst(). Check the status there.
*/

ProxyCacheLoadConnect (struct FAB *FabPtr)

{
   PROXY_TASK *tkptr;

   int  status;

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

   tkptr = FabPtr->fab$l_ctx;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheLoadConnect() !&F !&X !&X",
                 &ProxyCacheLoadConnect,
                 FabPtr->fab$l_sts, FabPtr->fab$l_stv);

   if (VMSnok (status = tkptr->LoadFab.fab$l_sts))
   {
      /**********************/
      /* sys$create() error */
      /**********************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE CREATE fail !&S", status);

      if (ProxyReportCacheLog)
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file create error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

      /* explicitly call the function to continue transaction with client */
      ProxyResponseNetWrite (tkptr);
      return;
   }

   /* copy the directory and file IDs returned from the file load $CREATE */
   memcpy (&tkptr->CacheFileQio.Fib.fib$w_did, &tkptr->ParseNam.nam$w_did, 6);
   memcpy (&tkptr->CacheFileQio.Fib.fib$w_fid, &tkptr->ParseNam.nam$w_fid, 6);

   tkptr->ParseNam.nam$l_ver[tkptr->ParseNam.nam$b_ver] = '\0';

   tkptr->LoadRab = cc$rms_rab;
   tkptr->LoadRab.rab$l_fab = &tkptr->LoadFab;
   tkptr->LoadRab.rab$l_ctx = tkptr;
   tkptr->LoadRab.rab$b_rac = RAB$C_SEQ;

   status = sys$connect (&tkptr->LoadRab, 0, 0);
   if (VMSok (status)) status = tkptr->LoadRab.rab$l_sts;

   if (VMSnok (status))
   {
      /***********************/
      /* sys$connect() error */
      /***********************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE CONNECT fail !&S", status);

      if (ProxyReportCacheLog)
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file connect error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

      /* delete file as closed, ensure an asynchronous close service */
      tkptr->LoadFab.fab$l_fop |= FAB$M_ASY | FAB$M_DLT;
      /* AST the function to continue the transaction with client */
      status = sys$close (&tkptr->LoadFab, &ProxyCacheCloseAst_NetWrite,
                                           &ProxyCacheCloseAst_NetWrite);
      return;
   }

   /* asynchronous truncate on put, in case file existed and we're reloading */
   tkptr->LoadRab.rab$l_rop = RAB$M_ASY | RAB$M_TPT;

   ProxyCacheLoadWrite (tkptr);
} 

/*****************************************************************************/
/*
Called after a chunk of data has been read from the remote server to write
that chunk into the cache file.  AST to ProxyCacheLoadWriteAst() to check the
success of the write.
*/

ProxyCacheLoadWrite (PROXY_TASK *tkptr)

{
   int  status;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheLoadWrite()");

   if (tkptr->ResponseContentLength < 0)
   {
      /* response had no content-length, check the quantity received so far */
      if (((tkptr->ResponseBytes - tkptr->ResponseHeaderLength) >> 10) + 1 >
          ProxyCacheFileKBytesMax)
      {
         /*********************************/
         /* exceeds configuration maximum */
         /*********************************/

         if (WATCHING (tkptr, WATCH_PROXY_CACHE))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
               "CACHE LOAD abort !UL exceeds !UL kB",
               ((tkptr->ResponseBytes - tkptr->ResponseHeaderLength) >> 10) + 1,
               ProxyCacheFileKBytesMax);

         /* close/delete file, fab$w_ifi will then indicate no cache file */
         tkptr->LoadFab.fab$l_fop |= FAB$M_DLT;
         /* synchronous service */
         tkptr->LoadFab.fab$l_fop &= ~FAB$M_ASY;
         status = sys$close (&tkptr->LoadFab, 0, 0);

         if (ProxyReportCacheLog)
            if (VMSnok (status))
               FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file delete-on-close error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
                  0, FI_LI, status, tkptr->ProxyCacheFileName);

         /* explicitly call the function to continue transaction with client */
         ProxyResponseNetWrite (tkptr);
         return;
      }
   }

   tkptr->LoadRab.rab$l_rbf = tkptr->ResponseBufferCachePtr;
   tkptr->LoadRab.rab$w_rsz = tkptr->ResponseBufferCacheCount;

   sys$put (&tkptr->LoadRab, &ProxyCacheLoadWriteAst,
                             &ProxyCacheLoadWriteAst);
} 

/*****************************************************************************/
/*
AST delivered from ProxyCacheLoadWrite().  Check the status of the write and
if successful declare the AST originally passed to that routine to continue
retrieving the request from the remote server.  If an error was encountered
delete as the file is closed and forget about further caching the response
(although in all these cases continuing to retrieve it via the network!)
*/

ProxyCacheLoadWriteAst (struct RAB *RabPtr)

{
   int  status;
   PROXY_TASK *tkptr;

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

   tkptr = RabPtr->rab$l_ctx;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheLoadWriteAst() !&F !&X !&X !UL",
                 &ProxyCacheLoadWriteAst,
                 RabPtr->rab$l_sts, RabPtr->rab$l_stv, RabPtr->rab$w_rsz);

   if (VMSnok (status = tkptr->LoadRab.rab$l_sts))
   {
      /**************************/
      /* cache file write error */
      /**************************/

      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE WRITE fail !&S", status);

      if (ProxyReportCacheLog)
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file write error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

      /* delete file as closed, ensure an asynchronous close service */
      tkptr->LoadFab.fab$l_fop |= FAB$M_ASY | FAB$M_DLT;
      status = sys$close (&tkptr->LoadFab, &ProxyCacheCloseAst_NetWrite,
                                           &ProxyCacheCloseAst_NetWrite);
      return;
   }

   ProxyResponseNetWrite (tkptr);
} 

/*****************************************************************************/
/*
Called by ProxyCacheEnd() when the response is complete.  Check if it meets the
criteria to be retained (i.e. essentially a completely successful transaction). 
If it does then just close the file.  If not then delete it as it is closed. 
The sys$close() are asynchronous, AST to ProxyEnd() to complete the
transaction.
*/

ProxyCacheLoadEnd (PROXY_TASK *tkptr)

{
   int  status,
        ClientWriteStatus,
        ProxyReadStatus;

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

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheLoadEnd()");

   ClientWriteStatus = SS$_NORMAL;
   if (tkptr->RequestPtr)
      if (VMSnok (tkptr->RequestPtr->NetIoPtr->WriteStatus))
         ClientWriteStatus = tkptr->RequestPtr->NetIoPtr->WriteStatus;
   ProxyReadStatus = tkptr->NetIoPtr->ReadStatus;
   /* normal for servers to abruptly disconnect at the end of a request */
   if (ProxyReadStatus == SS$_LINKDISCON) ProxyReadStatus = SS$_NORMAL;

   if ((tkptr->ResponseContentLength >= 0 &&
        tkptr->ResponseBodyLength != tkptr->ResponseContentLength) ||
       VMSnok (ClientWriteStatus) ||
       VMSnok (ProxyReadStatus))
   {
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         if (VMSnok (ProxyReadStatus))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE LOAD FAIL server read !&S",
                       ProxyReadStatus);
         else
         if (VMSnok (ClientWriteStatus))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE LOAD FAIL client write !&S",
                       ClientWriteStatus);
         else
         if (tkptr->ResponseContentLength >= 0)
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE LOAD FAIL received:!UL expecting:!UL",
                        tkptr->ResponseBodyLength,
                        tkptr->ResponseContentLength);
         else
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                       "CACHE LOAD FAIL reason unknown");
      }

      if (ProxyReportCacheLog)
      {
         if (VMSnok (ProxyReadStatus))
            FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, cache load read fail (!AZ !UL)\n-!&M\n \\!AZ\\\n",
               0, FI_LI, ProxyReadStatus, tkptr->ProxyCacheFileName);
         else 
         if (VMSnok (ClientWriteStatus))
            FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, cache load write fail (!AZ !UL)\n-!&M\n \\!AZ\\\n",
               0, FI_LI, ClientWriteStatus, tkptr->ProxyCacheFileName);
         else 
         if (tkptr->ResponseContentLength >= 0)
            FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, cache load fail, received:!UL expecting:!UL (!AZ !UL)\n\
 \\!AZ\\\n",
               0, tkptr->ResponseBodyLength, tkptr->ResponseContentLength,
               FI_LI, tkptr->ProxyCacheFileName);
         else
            FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, cache load fail (!AZ !UL)\n \\!AZ\\\n",
               0, FI_LI, tkptr->ProxyCacheFileName);
      }

      /* close/delete file asynchronously AST calling ProxyEnd() */
      tkptr->LoadFab.fab$l_fop |= FAB$M_DLT | FAB$M_ASY;
      status = sys$close (&tkptr->LoadFab, &ProxyCacheCloseAst_ProxyEnd,
                                           &ProxyCacheCloseAst_ProxyEnd);
      return;
   }
   else
   {    
      if (WATCHING (tkptr, WATCH_PROXY_CACHE))
      {
         char  ExpiresAt [64];
         if (QUAD_ZERO(tkptr->ResponseExpiresTime64))
            strcpy (ExpiresAt, "no expiry");
         else
            FaoToBuffer (ExpiresAt, sizeof(ExpiresAt), NULL, "expires !20%D",
                         &tkptr->ResponseExpiresTime64);
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
"CACHE LOAD OK !AZ, !UL bytes (!UL blocks), \
last-modified !UL/!UL (hrs/days), !AZ",
            tkptr->ResponseContentEncodingGzip ? "gzip encoded" : "unencoded",
            tkptr->ResponseBodyLength,
          ((tkptr->ResponseHeaderLength + tkptr->ResponseBodyLength) >> 9) + 1,
            tkptr->ProxyCacheLastModifiedHours,
            tkptr->ProxyCacheLastModifiedHours/24,
            ExpiresAt);
      }

      /* close file asynchronously AST calling ProxyEnd() */
      tkptr->LoadFab.fab$l_fop |= FAB$M_ASY;
      status = sys$close (&tkptr->LoadFab, &ProxyCacheCloseAst_ProxyEnd,
                                           &ProxyCacheCloseAst_ProxyEnd);
      return;
   }
} 

/*****************************************************************************/
/*
After an asynchronous sys$close() call ProxyEnd() with the 'tkptr' parameter
stored in the FAB context.
*/

ProxyCacheCloseAst_ProxyEnd (struct FAB *FabPtr)

{
   int  status;
   char  *cptr;
   PROXY_TASK  *tkptr;

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

   tkptr = FabPtr->fab$l_ctx;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheCloseAst_ProxyEnd() !&F !&X !&X",
                 &ProxyCacheCloseAst_ProxyEnd,
                 FabPtr->fab$l_sts, FabPtr->fab$l_stv);

   if (ProxyReportCacheLog)
      if (VMSnok (status = FabPtr->fab$l_sts))
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file delete-on-close error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

   /* if there was no load write error and so not set delete-on-close */
   if (!(tkptr->ParseFab.fab$l_fop & FAB$M_DLT))
   {
      /* set file header date/times the way *we* want! */
      ProxyCacheSetFileDateTime (&tkptr->CacheFileQio,
                                 &HttpdTime64,
                                 &tkptr->ResponseLastModifiedTime64,
                                 &tkptr->ResponseExpiresTime64,
                                 &HttpdTime64);
   }

   ProxyEnd (tkptr);
} 

/*****************************************************************************/
/*
After an asynchronous sys$close() call ProxyResponseNetWrite() with the 'tkptr'
parameter stored in the FAB context. 
*/

ProxyCacheCloseAst_NetWrite (struct FAB *FabPtr)

{
   int  status;
   PROXY_TASK  *tkptr;

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

   tkptr = FabPtr->fab$l_ctx;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyCacheCloseAst_NetWrite() !&F !&X !&X",
                 &ProxyCacheCloseAst_NetWrite,
                 FabPtr->fab$l_sts, FabPtr->fab$l_stv);

   if (ProxyReportCacheLog)
      if (VMSnok (status = FabPtr->fab$l_sts))
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, file delete-on-close error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, tkptr->ProxyCacheFileName);

   ProxyResponseNetWrite (tkptr);
} 

/*****************************************************************************/
/*
Create a cache directory.
*/ 
 
ProxyCacheCreateDirectory (PROXY_TASK *tkptr)

{
   static unsigned short  ProtectionEnable = 0xffff, /* alter all S,O,G,W */ 
                          ProtectionMask = 0xaa00;   /* S:RWED */
   static $DESCRIPTOR (DirectoryDsc, "");

   int  status;
   char  *cptr, *sptr;
   char  DirectoryName [64];

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY, "ProxyCacheCreateDirectory()");

   sptr = DirectoryName;
   for (cptr = tkptr->ProxyCacheFileName;
        *cptr && *cptr != ']';
        *sptr++ = *cptr++);
   *sptr++ = ']';
   *sptr = '\0';

   DirectoryDsc.dsc$a_pointer = DirectoryName;
   DirectoryDsc.dsc$w_length = sptr - DirectoryName;

   status = lib$create_dir (&DirectoryDsc, 0, &ProtectionEnable,
                            &ProtectionMask, 0, 0);
   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY, "!&Z !&S",
                 DirectoryName, status);

   if (status == SS$_CREATED)
      status = SS$_NORMAL;
   else
   if (status == SS$_NORMAL)
      status = RMS$_DNF;

   if (WATCHING (tkptr, WATCH_PROXY_CACHE))
   {
      if (VMSnok (status))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE DIRECTORY create fail !AZ !&S",
                    DirectoryName, status);
      else
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_CACHE,
                    "CACHE DIRECTORY created !AZ",
                    DirectoryName);
   }

   if (ProxyReportCacheLog)
      if (VMSnok (status))
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, directory create error (!AZ !UL)\n-!&M\n \\!AZ\\\n",
            0, FI_LI, status, DirectoryName);

   return (status);
}

/****************************************************************************/
/*
Return the age in seconds relative to the current time.
*/

int ProxyCacheDeltaSeconds
(
unsigned long *CurrentTime6464Ptr,
unsigned long *AgeTime64Ptr
)
{
   static unsigned long  LibDeltaSeconds = LIB$K_DELTA_SECONDS;

   int  status;
   unsigned long  DeltaSeconds;
   unsigned long  ResultTime64 [2];

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY, "ProxyCacheDeltaSeconds()");

   if (QUAD_ZERO(AgeTime64Ptr)) return (999999999);

   if (!CurrentTime6464Ptr) CurrentTime6464Ptr = &HttpdTime64;

   status = lib$sub_times (CurrentTime6464Ptr,
                           AgeTime64Ptr,
                           &ResultTime64);
   if (status == LIB$_NEGTIM) return (0);

   status = lib$cvt_from_internal_time (&LibDeltaSeconds,
                                        &DeltaSeconds,
                                        &ResultTime64);

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY, "!UL", DeltaSeconds);

   return (DeltaSeconds);
}

/****************************************************************************/
/*
Returns true if 'TheseHours' exceed the limit implemented by the age
algorithm as applied to 'LastModHours'. See "AGE ALGORITHM" in the description
of this module.
*/

int ProxyCacheReloadLastModified
(
unsigned long LastModHours,
unsigned long TheseHours
)
{
   int  idx;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                 "ProxyCacheReloadLastModified() !UL !UL",
                  LastModHours, TheseHours);

   /* element one is always a default bottom-of-the-range zero */
   for (idx = 1; idx < ProxyCacheReloadListCount; idx++)
   {
      if (WATCH_MODULE(WATCH_MOD_PROXY) && WATCH_MODULE(WATCH_MOD__DETAIL))
         WatchThis (WATCHALL, WATCH_MOD_PROXY, "!UL !UL !UL !UL",
                    idx, ProxyCacheReloadList[idx], LastModHours, TheseHours);
      if (LastModHours > ProxyCacheReloadList[idx]) continue;
      /* if these hours exceed the lower limit of the range then reload */
      if (TheseHours >= ProxyCacheReloadList[idx-1]) return (true);
      /* does not exceed the lower limit of the range therefore no reload */
      return (false);
   }

   /* same criteria as above */
   if (TheseHours >= ProxyCacheReloadList[idx-1]) return (true);
   return (false);
}

/*****************************************************************************/
/*
Update a file header's BDT, CDT, ETD and RDT date/time fields.
This function executes completely independently of any other processing, uses
it's own dynamically allocated data structure, and delivers an AST to check the
success of the operation and then deallocate the structure.

This function works by 'stealing' the contents of the PROXY_CACHE_FILE_QIO
structure from the calling routine and setting the two possible channels of
that structure to zero resulting in ProxyCacheEnd() not IO$_DEACCESS and then
deassigning the channel.  That's done here after the date/times are modified! 

This function uses the ACP-QIO interface detailed in the "OpenVMS I/O User's 
Reference Manual".
*/ 

ProxyCacheSetFileDateTime
(
PROXY_CACHE_FILE_QIO *ProxyCacheFileQioPtr,
unsigned long *BdtTime64Ptr,  /* last loaded at */
unsigned long *CdtTime64Ptr,  /* last modified at */
unsigned long *EdtTime64Ptr,  /* expires at */
unsigned long *RdtTime64Ptr   /* last modified at */
)
{
   int  status;
   char  *cptr, *sptr, *zptr;
   ATRDEF  *atptr;
   PROXY_CACHE_FILE_QIO  *cfqptr;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
"ProxyCacheSetFileDateTime() acp:!&B qio:!&B bdt:!%D cdt:!%D rdt:!%D edt:!%D",
                 ProxyCacheFileQioPtr->AcpChannel,
                 ProxyCacheFileQioPtr->QioChannel,
                 BdtTime64Ptr, CdtTime64Ptr,
                 RdtTime64Ptr, EdtTime64Ptr);

   if (!ProxyCacheFileQioPtr->AcpChannel &&
       !ProxyCacheFileQioPtr->QioChannel)
   {
      ErrorNoticed (NULL, SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      return;
   }

   cfqptr = VmGet (sizeof(PROXY_CACHE_FILE_QIO));

   memcpy (cfqptr, ProxyCacheFileQioPtr, sizeof(PROXY_CACHE_FILE_QIO));

   cfqptr->FibDsc.dsc$w_length = sizeof(cfqptr->Fib);
   cfqptr->FibDsc.dsc$a_pointer = &cfqptr->Fib;

   /* this will prevent ProxyCacheEnd() from doing it's thing */
   ProxyCacheFileQioPtr->AcpChannel = ProxyCacheFileQioPtr->QioChannel = 0;

   atptr = &cfqptr->FileAtr;

   PUT_QUAD_QUAD (BdtTime64Ptr, cfqptr->BdtTime64);
   atptr->atr$w_size = sizeof(cfqptr->BdtTime64);
   atptr->atr$w_type = ATR$C_BAKDATE;
   atptr->atr$l_addr = &cfqptr->BdtTime64;
   atptr++;

   PUT_QUAD_QUAD (CdtTime64Ptr, cfqptr->CdtTime64);
   atptr->atr$w_size = sizeof(cfqptr->CdtTime64);
   atptr->atr$w_type = ATR$C_CREDATE;
   atptr->atr$l_addr = &cfqptr->CdtTime64;
   atptr++;

   PUT_QUAD_QUAD (EdtTime64Ptr, cfqptr->EdtTime64);
   atptr->atr$w_size = sizeof(cfqptr->EdtTime64);
   atptr->atr$w_type = ATR$C_EXPDATE;
   atptr->atr$l_addr = &cfqptr->EdtTime64;
   atptr++;

   PUT_QUAD_QUAD (RdtTime64Ptr, cfqptr->RdtTime64);
   atptr->atr$w_size = sizeof(cfqptr->RdtTime64);
   atptr->atr$w_type = ATR$C_REVDATE;
   atptr->atr$l_addr = &cfqptr->RdtTime64;
   atptr++;

   atptr->atr$w_size = atptr->atr$w_type = atptr->atr$l_addr = 0;

   if (cfqptr->AcpChannel)
   {
      /* ProxyCacheSetFileDateTimeAst() expects it accessed on 'QioChannel' */
      cfqptr->QioChannel = cfqptr->AcpChannel;
      cfqptr->AcpChannel = 0;

      status = sys$qio (EfnNoWait, cfqptr->QioChannel, IO$_MODIFY,
                        &cfqptr->IOsb, &ProxyCacheSetFileDateTimeAst, cfqptr,
                        &cfqptr->FibDsc, &cfqptr->FileNameDsc,
                        0, 0, &cfqptr->FileAtr, 0);
      if (VMSnok (status))
      {
         /* let the AST routine handle it! */
         cfqptr->IOsb.Status = status;
         SysDclAst (&ProxyCacheSetFileDateTimeAst, cfqptr);
      }
   }
   else
   if (cfqptr->QioChannel)
   {
      /* this channel has the file 'open' for read, deaccess then modify */
      status = sys$qio (EfnNoWait, cfqptr->QioChannel, IO$_DEACCESS,
                        &cfqptr->IOsb, &ProxyCacheSetFileDateTime2, cfqptr, 
                        &cfqptr->FibDsc, 0, 0, 0, 0, 0);
      if (VMSnok (status))
      {
         /* let the AST routine handle it! */
         cfqptr->IOsb.Status = status;
         SysDclAst (&ProxyCacheSetFileDateTime2, cfqptr);
      }
   }
   else
   {
      /* hmmm, no channel assigned from ProxyCacheReadAcpInfo() */
      cfqptr->IOsb.Status = SS$_BUGCHECK;
      SysDclAst (&ProxyCacheSetFileDateTimeAst, cfqptr);
   }
}

/*****************************************************************************/
/*
AST from IO$_DEACCESS above.  Now do the IO$_MODIFY.
*/ 

ProxyCacheSetFileDateTime2 (PROXY_CACHE_FILE_QIO *cfqptr)

{
   int  status;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                 "ProxyCacheSetFileDateTime2() !&F !&S",
                 &ProxyCacheSetFileDateTime2, cfqptr->IOsb.Status);

   if (VMSok (status = cfqptr->IOsb.Status))
      status = sys$qio (EfnNoWait, cfqptr->QioChannel, IO$_MODIFY,
                        &cfqptr->IOsb, &ProxyCacheSetFileDateTimeAst, cfqptr,
                        &cfqptr->FibDsc, 0, 0, 0, &cfqptr->FileAtr, 0);
   if (VMSnok (status))
   {
      /* let the AST routine handle it! */
      cfqptr->IOsb.Status = status;
      SysDclAst (&ProxyCacheSetFileDateTimeAst, cfqptr);
   }
}

/****************************************************************************/
/*
AST delivered from IO$_MODIFY above.  Just check and if required report error
status and deassign the channel.
*/

ProxyCacheSetFileDateTimeAst (PROXY_CACHE_FILE_QIO *cfqptr)

{
   int  status;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHALL, WATCH_MOD_PROXY,
                 "ProxyCacheSetFileDateTimeAst() !&F !&S",
                 &ProxyCacheSetFileDateTimeAst, cfqptr->IOsb.Status);

   if (ProxyReportCacheLog)
      if (VMSnok (cfqptr->IOsb.Status))
         FaoToStdout ("%HTTPD-W-PROXY, \
!20%D, expired timestamp update error (!AZ !UL)\n-!&M\n \
\\!#AZ\\\n",
            0, FI_LI, cfqptr->IOsb.Status,
            cfqptr->FileNameDsc.dsc$w_length,
            cfqptr->FileNameDsc.dsc$a_pointer);

   sys$dassgn (cfqptr->QioChannel);
   VmFree (cfqptr, FI_LI);
}

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

