/*****************************************************************************/
/*
                                   CGI.c

Provide generalized CGI scripting support.

Creates a buffer containing either, the DCL commands to create CGI variables at
the VMS command line (e.g. for standard CGI scripts), or a stream of
'name=value' pairs for the CGI variables (e.g. for CGIplus scripts). 

The buffer is dynamically allocated and contains a series of sequential records
comprising a word (16 bits) with the length of a following (varying)
null-terminated string (including the null character).  The end of the series
is indicated by a zero length record.

This buffer is scanned from from first to last and the strings passed to the
script 'input' stream as appropriate.


CGI RESPONSE HEADER
-------------------
In common with other implementations, if the first line of the response header
begins "HTTP/1." then it is considered to be a full HTTP response (non-parsed
header output), the server does no more header processing and it is up to the
script to provide a fully compliant HTTP response.

Handling of the CGI response header is pretty standard.  It should begin with
"Content-Type:", "Status:", or "Location:".  One of more of these should be
received before the end-of-header blank line or other header lines.  As soon as
any other line is received the server considered the CGI header complete.

The CGI header can be received record-by-record, with or without any
carriage-control, or as a block of lines with correct carriage-control (only
complete lines will be processed correctly).

The "X-vms-record-mode:" field is a VMS Apacheism for indicating to the server
whether VMS script output must be checked for correct carriage-control and
corrected if necessary.  It seems as if 0 is "not record mode", 1 is.

If record mode carriage-control is being processed and completely empty records
start being received then switch to a record-rpcoessing mode where only
completely empty recards have carriage control inserted.

Current "Script-Control:" Directives
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
no-abort                       CGI/1.2, do not abort script because of timeout
X-buffer-records[=0|1]         buffer records before sending to client
X-cache-max=<integer>          kBytes maximum
X-cache-expires=<param>        "day", "hour", "min", 'hh:mm:ss' or minutes
X-content-handler=<param>      "SSI", content should be processed by SSI engine
X-content-encoding-gzip[=0|1]  disable/enable gzip compression (if supported)
X-crlf-mode[=0|1]              ensure all records have a trailing <CR><LF>
X-error-line=<integer>         source line error noted (optional)
X-error-module=<string>        source code module error noted (optional)
X-error-text=<string>          textual explanation of error (required)
X-error-vms-status=<integer>   if generated by a VMS status error (optional)
X-error-vms-text=<string>      VMS explanation (often a file spec, optional)
X-filter-stream[=0|1]          remove all non-printable characters
X-http-status=<integer>        override response line status and for redirected
                               error reporting script override original status
X-lifetime=<int>               "none" or integer, set script process lifetime
X-record-mode[=0|1]            ensure all records have at least trailing <LF>
X-record0-mode[=0|1]           only empty records have a trailing <LF> inserted
X-stream-mode[=0|1]            do not alter records at all, WSSIWCG!
X-timeout-output=<param>       "none" or integer, set request output timer
X-timeout-noprogress=<param>   "none" or integer, set request no-progress timer
X-transfer-encoding-chunked[=0|1]  disable/enable chunking
(all times are in minutes)

The "Script-Control:" is a proposed CGI/1.2 header field intended for script
control ;^). By default WASD writes each record to the client as it is received
from the script (in case of slow or intermittent output).  For script's where
this is known not to be an issue this directive can be used to have the server
buffer the records, outputting the block when the buffer fills or at script
conclusion.  This is often *much* more efficient.  The record mode forces each
record received to be checked for correct carriage-control (a trailing <LF>)
and if not present to be appended.


SCRIPT REQUESTED ERROR MESSAGE
------------------------------
Using the script control error fields a script can request the server to
generate an error response on it's behalf.  This has the advantage of
generating error messages conforming to the configured components and layout of
server error responses.  The following show examples of the response header
lines required to use this facility.  The 'X-error-text' field is what triggers
the error generation and is the only mandatory field.

Vanilla error message

  |Status: 400<LF>
  |Script-Control: X-error-text="Object not found."<LF>
  |<LF>

VMS status error message

  |Status: 403<LF>
  |Script-Control: X-error-text="opening file"<LF>
  |Script-Control: X-error-vms-status=36<LF>
  |Script-Control: X-error-vms-text="HT_ROOT:[SRC]EXAMPLE.TXT"<LF>
  |<LF>

or using a VMS hexadecimal string (with module name and line information)

  |Status: 403<LF>
  |Script-Control: X-error-text="opening file"<LF>
  |Script-Control: X-error-vms-status=%X00000024<LF>
  |Script-Control: X-error-vms-text="HT_ROOT:[SRC]EXAMPLE.TXT"<LF>
  |Script-Control: X-error-module=EXAMPLE<LF>
  |Script-Control: X-error-line=123<LF>
  |<LF>


CGI VARIABLES
-------------
Most of these CGI variable names are those supported by the INTERNET-DRAFT
authored by D.Robinson (drtr@ast.cam.ac.uk), 8 January 1996, plus some
"convenience" variables, breaking the query string into its components (KEY_,
FORM_, etc.) The CGI symbols (CERN/VMS-HTTPd-like DCL symbols instead of Unix
environment variables) are created by the SYS$COMMAND stream of each
script process before the script DCL procedure is invoked. By default each
variable name is prefixed by "WWW_" (similar to CERN HTTPd), although this can
be modified at the command line when starting the server.

     VARIABLE NAME            DESCRIPTION                            ORIGIN
     -------------            -----------                            ------

  (if the request is not authorized only AUTH_TYPE will exist)
  o  AUTH_ACCESS ............ "READ" or "READ+WRITE"                 WASD
  o  AUTH_AGENT ............. indicates authentication agent         WASD
  o  AUTH_GROUP ............. path authorization group               WASD
  o  AUTH_PASSWORD .......... only if "EXTERNAL" realm               WASD
  o  AUTH_PATH .............. path from rule (only in agent)         WASD
  o  AUTH_REALM ............. authentication realm                   WASD
  o  AUTH_REALM_DESCRIPTION . realm description                      WASD
  o  AUTH_REMOTE_USER ....... original, now proxied username         WASD
  o  AUTH_TYPE .............. "BASIC" or "DIGEST" (or empty)         CGI
  o  AUTH_USER .............. details of user                        WASD

  o  CONTENT_LENGTH ......... "Content-Length:" from header          CGI
  o  CONTENT_TYPE ........... "Content-Type:" from header            CGI
  o  CSP_NONCE .............. content-security-policy nonce          WASD

  o  DOCUMENT_ROOT .......... base directory for serving files       Apache

  o  FORM_field ............. query "&" separated form elements      WASD
  o  FORM__integer .......... relaxed empty form field names         WASD

  o  GATEWAY_BG ............. socket BG device name                  WASD
  o  GATEWAY_EOF ............ CGIPLUSEOF sentinal (if CGIplus)       WASD
  o  GATEWAY_EOT ............ CGIPLUSEOT sentinal (if not DECnet)    WASD
  o  GATEWAY_ESC ............ CGIPLUSESC sentinal (if not DECnet)    WASD
  o  GATEWAY_INTERFACE ...... "CGI/1.1"                              CGI
  o  GATEWAY_MRS ............ maximum record size of mailbox         WASD
  o  GATEWAY_QIO ............ maximum network QIO                    WASD
  o  GATEWAY_SYMBOLS ........ comma-separated list of CGI symbols    WASD

  o  HTML_BODYTAG ........... report <body..> tag (e.g. "BGCOLOR=")  WASD
  o  HTML_FOOTER ............ report footer text                     WASD
  o  HTML_FOOTERTAG ......... report footer <td..> tag               WASD
  o  HTML_HEADER ............ report header text                     WASD
  o  HTML_HEADERTAG ......... report header <td..> tag               WASD

  o  HTTP2_PING ............. most recent HTTP/2 ping result         WASD

  o  HTTP_ACCEPT ............ list of browser-accepted content types CGI
  o  HTTP_ACCEPT_CHARSET .... list of browser-accepted character set CGI
  o  HTTP_ACCEPT_ENCODING ... list of browser-accepted encodings     CGI
  o  HTTP_ACCEPT_LANGUAGE ... list of browser-accepted languages     CGI
  o  HTTP_AUTHORIZATION ..... only if "EXTERNAL" realm               CGI
  o  HTTP_CACHE_CONTROL ..... HTTP/1.1 cache control field           CGI
  o  HTTP_CONNECTION ........ connection control field               CGI
  o  HTTP_CONTENT_LENGTH .... content-length                         CGI
  o  HTTP_CONTENT_TYPE ...... content-type                           CGI
  o  HTTP_COOKIE ............ any cookie sent by the client          CGI
  o  HTTP_FORWARDED ......... list of proxy/gateway hosts            CGI
  o  HTTP_HOST .............. destination host name/port             CGI
  o  HTTP_IF_MATCH .......... HTTP/1.1 if-match request field        CGI
  o  HTTP_IF_NONE_MATCH ..... HTTP/1.1 if-none-match request field   CGI
  o  HTTP_IF_MODIFIED_SINCE ..... request field                      CGI
  o  HTTP_IF_UNMODIFIED_SINCE ... HTTP/1.1 request field             CGI
  o  HTTP_IF_RANGE .......... HTTP/1.1 if-range request field        CGI
  o  HTTP_KEEP_ALIVE ........ request field                          CGI
  o  HTTP_PRAGMA ............ request filed                          CGI
  o  HTTP_RANGE ............. HTTP/1.1 requested body byte range     CGI
  o  HTTP_REFERER ........... source document URL for this request   CGI
  o  HTTP_TRAILER ........... HTTP/1.1 request field                 CGI
  o  HTTP_TRANSFER_ENCODING ... HTTP/1.1 request field               CGI
  o  HTTP_USER_AGENT ........ client/browser identification string   CGI
  o  HTTP_X_FORWARDED_FOR ... proxy forwarded for this client        SQUID

  o  KEY_integer ............ query string "+" separated elements    WASD
  o  KEY_COUNT .............. number of "+" separated elements       WASD

  o  PATH_INFO .............. virtual path of data requested in URL  CGI
  o  PATH_ODS ............... on-disk structure of path (0, 2 or 5)  WASD
  o  PATH_TRANSLATED ........ VMS file path of data requested in URL CGI

  o  QUERY_STRING ........... string following "?" in URL            CGI

  o  REMOTE_ADDR ............ IP host address of HTTP client         CGI
  o  REMOTE_HOST ............ IP host name of HTTP client            CGI
  o  REMOTE_PORT ............ IP port of HTTP client                 WASD
  o  REMOTE_USER ............ authenticated username (or empty)      CGI
  o  REQUEST_CHARSET ........ charset SET by mapping rule            WASD
  o  REQUEST_CONTENT_TYPE ... content-type SET by mapping rule       WASD
  o  REQUEST_METHOD ......... "GET", "PUT", etc.                     CGI
  o  REQUEST_PROTOCOL ....... HTTP protocol version ("HTTP/2")       WASD
  o  REQUEST_SCHEME ......... "http:" or "https:"                    WASD
  o  REQUEST_TIME_GMT ....... request GMT time                       WASD
  o  REQUEST_TIME_LOCAL ..... request local time                     WASD
  o  REQUEST_URI ............ un-encoded path[?query-string]         Apache

  o  SCRIPT_DEFAULT ......... script process default directory       WASD
  o  SCRIPT_FILENAME ........ (e.g. "CGI-BIN:[000000]TEST.COM")      Apache
  o  SCRIPT_NAME ............ name of script (e.g. "/query")         CGI
  o  SCRIPT_RTE ............. script run-time environment            WASD
  o  SERVER_ADDR ............ IP host address of server system       WASD
  o  SERVER_ADMIN ........... "webmaster@site.domain"                Apache
  o  SERVER_CHARSET ......... default charset (e.g. "ISO-8895-1")    WASD
  o  SERVER_GMT ............. offset from GMT time (e.g. "+09:30)    WASD
  o  SERVER_MULTIHOME ....... IP address client used (if different)  WASD
  o  SERVER_NAME ............ IP host name of server system          CGI
  o  SERVER_PROTOCOL ........ HTTP protocol version ("HTTP/1.1")     CGI
  o  SERVER_PORT ............ IP port request was received on        CGI
  o  SERVER_SOFTWARE ........ software ID of the HTTPD daemon        CGI
  o  SERVER_SIGNATURE ....... "server host port"                     Apache
  o  SERVER_TRUNCATE ........ if script=symbol=truncate in effect    WASD

  o  UNIQUE_ID .............. a "unique" request ID                  Apache
  o  UPSTREAM_ADDR .......... client address rewritten - original    WASD

  o  some SSL related CGI variable can be generated when SSL is available
     (see Sesola.C module)

NOTE: If script portability is a concern then confine use to the "standard" CGI
variables.  For WASD-specific scripts the extension variables may be used.  Not
all variables will be present for all requests, especially 'HTTP_' ones.  For
instance, the presence or not of HTTP_CONTENT_LENGTH may be used to determine
whether it was actually present in the request header or not.


CGI VARIABLES USING DCL SYMBOLS
-------------------------------
Due to the mechanism used to create DCL symbols for the CGI variables (for
standard CGI) the variable value is limited to the symbol size number of
characters minus the length of the DCL symbol name assignment (e.g.
"WWW_QUERY_STRING=A+B+C+D").  DCL symbol creation at the command line is
limited by the CLI command line length (255 characters for pre 7.3-2, 4095 for
post 7.3-1).  Symbol values however can be up to 1024 (or pragmatically about
1000 characters) for pre 7.3-2 and 8192 (or thereabouts) for post 7.3-1,
probably enough for any CGI variable value.  If a CGI value is too large for
for a single command-line assignment then build it up using multiple
assignments, a symbol assignment kludge!  All this is of course ultimately
limited by the size of the DCL command mailbox.


CGI VARIABLES USING CGIPLUS STREAM
----------------------------------
The maximum 'name=value' length for a CGI variable conveyed using the CGIPLUSIN
stream is limited by the size of of the 'DclCgiPlusInSize' global storage value
which is in turn set by the [BufferSizeDclCgiPlusIn] configuration directive. 
Hence if this is set to 3072 bytes (the current default) then the maximum
'name=value' length is the that value minus 3 bytes.  This value is also the
total mailbox buffer space allocated for script process IPC and so is also the
limit on all variables!  So, in the circumstance where the largest variable
needs to be 4000 bytes in length, and the rest total up to another 3000 bytes,
the [BufferSizeDclCgiPlusIn] is suggested to be 8192 bytes.  When this value is
adjusted it is recommended to review the HTTP server account's BYTLM quota.


CGIPLUS VARIABLE STRUCTURE
--------------------------
CGIplus (and standard CGI) variables are originally created as a single,
structured block of data.  The internal structure is quite simple.  Each
null-terminated "name=value" pair is preceded by an unsigned short (16 bit)
length field.  This value includes all characters in the "name=value" pair,
plus the terminating null character.  Hence the maximum length of one of these
pairs is 65535 minus 1.  The end of the "name=value" pairs is indicated by a
zero-value (and hence "name=value" length) short.

  |length|name=value\0|length|name=value\0|length|name=value\0|zero|

This data structure can easily be walked.  Passing these CGI variables to the
scripting process can be done in two ways, parsing each CGI variable and
providing it individually as a "record", and by providing the entire data
structure intact as a "struct".  Of course with the latter it remains for the
script itself to parse the structure to obtain the CGI variable values.

Transfering the CGIplus variables as a single structure (rather than as
per-variable records) can double the throughput!!  Demonstrated using the
[SRC.CGIPLUS]CGIPLUSTEST.C program.  This indicates that there is much less
than half the overhead for performing this using the 'struct' method!


VERSION HISTORY
---------------
23-NOV-2024  MGD  GATEWAY_QIO variable
19-MAY-2023  MGD  GATEWAY_SYMBOLS variable
12-SEP-2022  MGD  CSP_NONCE variable
                  CGI header "content-security-policy:" parsing
27-MAY-2021  MGD  using v12... agent functionality
                  dictionary entries ~CGI_<name> become CGI variables
02-APR-2021  MGD  bugfix; CgiGenerateVariables() "AUTHAGENT hangs when called
                    for a POST request" per JPP
30-APR-2019  MGD  bugfix; CgiOutput() Content-Length: strtoul()
25-FEB-2018  MGD  CgiOutput() constrain response header field names
28-NOV-2017  MGD  CgiOutput() reject subsequent non-header
04-OCT-2017  MGD  CgiOutput() refine Content-Length: to report out-of-range
28-SEP-2017  MGD  bugfix; CgiGenerateVariables()
                    |rqptr->rqAuth.SourceRealm != AUTH_SOURCE_AGENT_OPAQUE &&|
24-MAY-2017  MGD  bugfix; for HTTP/2 (sigh) we need NPH to generate a header
01-DEC-2016  MGD  RAWSOCKET_.. variables
11-AUG-2016  MGD  Script-Control: X-http-status=<integer> 
20-JUL-2016  MGD  CgiGenerateVariables() mitigate httpoxy vulnerability
                  bugfix; CgiGenerateVariables() names from dictionary
24-JAN-2016  MGD  REQUEST_PROTOCOL, HTTP2_PING and "cgi_.." dictionary
17-OCT-2015  MGD  CgiVariable() if |SymbolValue| is NULL parse the
                    name then value from |SymbolName| <name>=<value>
                  CgiGenerateVariables() WATCH total bytes in variables
02-DEC-2014  MGD  X-record0-mode[=0|1]
                  a completely empty record can only be where carriage-control
                    is being generated as records without C-RTL newline char
                    insertion - so switch to a non-embedded CC record mode
06-NOV-2014  MGD  UPSTREAM_ADDR client address rewitten - this is the original
25-MAY-2013  MGD  bugfix; AUTH_GROUP selected using write/read status
26-JAN-2010  MGD  Web Socket scripting
                  ScriptControlField() returns non-zero to indicate success
01-JUN-2008  JPP  CgiVariable() optimise single-quotation escaping
19-APR-2008  MGD  WATCH_SCRIPT indicates when script WATCHing is enabled
17-JAN-2008  MGD  CgiGenerateVariables() if SCRIPT_NAME is "/" (empty
                  froM mapping) then make it "" (really empty :-)
24-NOV-2007  MGD  AUTH_PATH provides path from rule for auth agent
08-APR-2007  MGD  CgiGenerateVariables() provide TRACK_ID if present (for JPP)
08-FEB-2006  MGD  potential bugfix; CgiOutput() CGI_OUTPUT_MODE_CRLF output
                  count should be checked zero before using negative index,
                  bugfix; CgiOutput() empty 'record' in stream mode should be
                  ignored and not have carriage-control adjusted (JFP)
01-JUN-2005  MGD  CgiVariable() allow path mapping script=symbol=truncate to
                  truncate a CLI symbol within the limit of the current VMS
                  version capacity, noting this in SERVER_TRUNCATE variable
20-APR-2005  MGD  provide SERVER_MULTIHOME if present (see NET.C)
20-JAN-2005  MGD  if CGI response "Location:" is supplied but no HTTP
                  status turn it into a 302 (see also ResponseHeader())
08-JAN-2005  MGD  process script=control=<...> path setting
16-DEC-2004  MGD  Script-Control: X-transfer-encoding-chunked[=0|1],
                  detect when a script is providing content-encoded output
26-NOV-2004  MGD  Script-Control: X-content-encoding-gzip[=0|1],
                  allow other binary controls to specify off or on (=[0|1])
07-NOV-2004  MGD  massage PATH_TRANSLATED and SCRIPT_FILENAME
                  if path SET script=syntax=unix
20-OCT-2004  MGD  change content_length CGI variable to "?" if a POSTed
                  (etc.) requests's body is being decoded by the server,
                  use pseudo-random numbers in CgiSequence()
17-OCT-2004  MGD  bugfix; CgiOutputFile() missing sizeof(FILE_CONTENT)
                  when VmReallocHeap() increasing buffer space
20-JUL-2004  MGD  rework provision of HTTP_ CGI variables,
                  provide HTTP_CONTENT_LENGTH if in header,
                  allow for "HTTP/1.1 100 Continue" responses
07-APR-2004  MGD  bugfix; agent script should have non-strict-CGI ignored
                  (stupid problem introduced with script output caching)
26-FEB-2004  MGD  add SCRIPT_DEFAULT from SET 'script=default=<string>'
10-JAN-2004  MGD  absorb CGI/NPH header,
                  Script-Control: X-content-handler=SSI field
23-DEC-2003  MGD  for VMS 7.3-2 and later take advantage of the larger 
                  EDCL CLI line (255->4095) and symbol (1024->8192) sizes
28-SEP-2003  MGD  add HTTP_KEEP_ALIVE and HTTP_CONNECTION
21-AUG-2003  MGD  HTTP_RANGE field CGI variable
09-JUL-2003  MGD  add new dynamic caching capability
21-JUN-2003  MGD  bugfix; CgiOutput() (jpp@esme.fr)
11-MAY-2003  MGD  unrecognised request fields as "HTTP_.." variables
02-APR-2003  MGD  add HTTP_X_FORWARDED_FOR CGI variable
01-MAR-2003  MGD  path setting HTML body/header/footer
24-JAN-2003  MGD  relax initial CGI response line checking
17-JAN-2003  MGD  path setting 'script=query=none' suppresses parsing of
                  query string into form or key elements
                  added GATEWAY_EOF/EOT/ESC variables,
                  sentinals changed to have only RMS-compliant characters,
                  path set carriage-control on CGIPLUSIN stream,
                  supply more detail from "%DCL-E-OPENIN, blah" responses
10-JAN-2003  MGD  path setting 'script=query=relaxed' allows form-url-encoded
                  query strings to be non-conformant without error reporting
05-DEC-2002  MGD  in non-strict CGI mode report anything like
                  "%DCL-E-OPENIN, blah" as a failed script activation
08-OCT-2002  MGD  Script-Control: X-error-... fields
17-AUG-2002  MGD  modify PATH_ODS variable to allow for PWK and SRI
01-AUG-2002  MGD  bugfix; do not count callout records for CGI header purposes
16-JUL-2002  MGD  last-minute relaxation of stricter CGI header processing
                  allowing a trailing CR to terminate a single header record
30-APR-2002  MGD  "Cache-Control:" field for Mozilla compatibility
30-MAR-2002  MGD  stream-mode with zero output as suppressed carriage-control
24-NOV-2001  MGD  significant rework CgiOutput() CGI header processing
14-OCT-2001  MGD  support script=params=(name=value) SET rule
15-SEP-2001  MGD  modify CgiOutput() processing,
                  X-filter-stream removes non-printable characters
04-AUG-2001  MGD  support module WATCHing
27-FEB-2001  MGD  suppress "x-internal/" CONTENT_TYPE
20-FEB-2001  MGD  AUTH_REMOTE_USER for proxied authorization,
                  refine detection of authorization agent reponse errors
01-FEB-2001  MGD  bugfix; CGI strict output suppression for auth agent
18-JAN-2001  MGD  bugfix; REMOTE_PORT ntohs()
19-DEC-2000  MGD  refine [CgiStrictOutput] to detect an incomplete header
05-SEP-2000  MGD  additional "Script-Control:" directives
08-AUG-2000  MGD  if response "Content-Encoding:" force stream mode,
                  add GATEWAY_BG and GATEWAY_MRS variables
01-JUL-2000  MGD  modify CgiOutput() to better handle CGI responses,
                  add "Script-Control:" (CGI/1.2)
24-JUN-2000  MGD  SCRIPT_RTE for persistent run-time environments
10-MAY-2000  MGD  refined CgiOutput() and CgiHeader()
08-APR-2000  MGD  CGI binary header response can now be processed,
                  (some) VMS Apache compatibility
31-DEC-1999  MGD  PATH_ODS variable (to indicate on-disk file system)
17-DEC-1999  MGD  bugfix; quote double-up in CgiVariable()
27-NOV-1999  MGD  rework DCL variable stream,
                  remove sys$fao() from stream CGI variable creation,
                  avoid "''" substitution creating DCL symbol CGI variables
20-NOV-1999  MGD  end-of-header blank line may now be an empty record,
                  or a record containing a single '\n' or '\r\n' sequence
06-NOV-1999  MGD  allow slightly more latitude with form fields, no "=value"
                  (i.e. name terminated by '&') and back-to-back '&'
14-SEP-1999  MGD  modify CgiOutput() for delayed CGI "Content-Type:"
28-AUG-1999  MGD  support CGIplus "agents", added AUTH_USER
30-MAY-1999  MGD  add SSL variables (via call to SesolaCgiGenerateVariables())
21-FEB-1999  MGD  change authorization CGI variables
28-DEC-1998  MGD  added HTTP_ACCEPT_ENCODING
07-NOV-1998  MGD  WATCH facility
17-OCT-1998  MGD  CgiUrlDecodeString(),
                  SERVER_CHARSET, REQUEST_CHARSET, REQUEST_CONTENT_TYPE
28-APR-1998  MGD  CGI variable memory allocation changed in support of SSI
03-APR-1998  MGD  bugfix; extra quotes generated in form field value
19-MAR-1998  MGD  suppress 'Authorization:' unless "external" authorization
06-DEC-1997  MGD  functionality unbundled from DCL.c, generalized for version 5
*/
/*****************************************************************************/

#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 <stdio.h>
#include <errno.h>
#include <limits.h>

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

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

#define WASD_MODULE "CGI"

/**********/
/* macros */
/**********/

#define CLI$_BUFOVF 229400

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

char  CgiNotStrict [] = "not a strict CGI response",
      ErrorCgiCli [] = "CLI reported \"!AZ\"",
      ErrorCgiSymbol [] = "DCL symbol assignment too large!",
      ErrorCgiNotStrict [] = "NOT a strict CGI response!!",
      ErrorCgiSetCookie [] = "Set-Cookie array exhausted";

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

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern BOOL  OdsExtended;

extern int  DclCgiHeaderSize,
            DclCgiPlusInSize,
            DclSysCommandSize,
            DclSysOutputSize,
            EfnWait,
            ServerPort,
            SsiSizeMax;

extern int64  HttpdTime64;

extern char  DclCgiVariablePrefix[],
             ErrorSanityCheck[],
             SoftwareID[],
             TimeGmtString[];

extern CONFIG_STRUCT Config;
extern HTTPD_PROCESS  HttpdProcess;
extern MSG_STRUCT Msgs;
extern SYS_INFO  SysInfo;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Creates a buffer containing a series of null-terminated strings, terminated by
a null-string (two successive null characters).  Each of the strings contains
either DCL command(s) to create a DCL symbol (CGI variable) using the CLI, or
an equate separated 'name=value' pair used for CGIplus variable streams, etc.
*/ 

int CgiGenerateVariables
(
REQUEST_STRUCT *rqptr,
int VarType
)
{
   static $DESCRIPTOR (NumberFaoDsc, "!UL\0");
   static $DESCRIPTOR (Number64FaoDsc, "!@UQ\0");
   static $DESCRIPTOR (StatusValueFaoDsc, "%X!8UL\0");

   BOOL  BodyUnEncodeStream;
   int  idx, status,
        Count,
        EmptyFieldNameCount,
        KeyCount;
   char  *cptr, *sptr, *zptr,
         *NameValuePtr;
   char  CspNonce [CSP_NONCE_SIZE+1],
         FieldName [256],
         LocalDateTime [48];

   char  String [8192];

   $DESCRIPTOR (StringDsc, String);
   struct {
      unsigned short  Status;
      unsigned short  Count;
      unsigned long  Unused;
   } IOsb;

   DICT_ENTRY_STRUCT  *denptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI, "CgiGenerateVariables()");

   if (rqptr->AgentRequestPtr)
   {
      /* no method indicates NetAgentBegin() or TcpIpAltAgentBegin()  */
      if (!rqptr->rqHeader.Method)
      {
         /***************/
         /* limited CGI */
         /***************/

         if (VMSnok (status =
             CgiVariable (rqptr, "GATEWAY_INTERFACE", "CGI/1.1", VarType)))
            return (status);

         if (rqptr->ClientPtr->IpAddressString[0])
            if (VMSnok (status =
                CgiVariable (rqptr, "REMOTE_ADDR",
                             rqptr->ClientPtr->IpAddressString, VarType)))
               return (status);

         if (rqptr->ClientPtr->Lookup.HostName[0])
            if (VMSnok (status =
                CgiVariable (rqptr, "REMOTE_HOST",
                             rqptr->ClientPtr->Lookup.HostName, VarType)))
               return (status);

         if (rqptr->ServicePtr->ServerHostName[0])
            if (VMSnok (status =
                CgiVariable (rqptr, "SERVER_NAME",
                             rqptr->ServicePtr->ServerHostName, VarType)))
               return (status);

         if (rqptr->ServicePtr->ServerPortString[0])
            if (VMSnok (status =
                CgiVariable (rqptr, "SERVER_PORT",
                             rqptr->ServicePtr->ServerPortString, VarType)))
               return (status);

         if (VMSnok (status =
             CgiVariable (rqptr, "SERVER_SOFTWARE", SoftwareID, VarType)))
            return (status);

         if (VMSnok (status = SesolaCgiGenerateVariables (rqptr, VarType)))
            return (status);

         return (CgiVariable (rqptr, NULL, NULL, VarType));
      }
      /* drop through for full CGI */
   }

   if (WATCHING (rqptr, WATCH_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_CGI, "VARIABLES !&?stream\rdcl\r",
                 VarType == CGI_VARIABLE_STREAM);

   if (rqptr->rqCgi.BufferLength)
   {
      /* free previous variables and storage */
      if (rqptr->rqCgi.BufferPtr)
         VmFreeFromHeap (rqptr, rqptr->rqCgi.BufferPtr, FI_LI); 
      rqptr->rqCgi.BufferLength = 0;
      rqptr->rqCgi.BufferPtr = rqptr->rqCgi.BufferCurrentPtr = NULL;
   }

   if (VarType == CGI_VARIABLE_STREAM)
      CgiVariableBufferMemory (rqptr, DclCgiPlusInSize*2);
   else
      CgiVariableBufferMemory (rqptr, DclSysCommandSize);

   HttpLocalTimeString (LocalDateTime, &rqptr->rqTime.BeginTime64);

   if (VMSnok (status =
       CgiVariable (rqptr, "AUTH_TYPE", rqptr->rqAuth.Type, VarType)))
      return (status);

   if (rqptr->AgentRequestPtr ||
       rqptr->rqAuth.Type[0])
   {
      if (rqptr->rqAuth.SourceRealm &&
          rqptr->rqAuth.RequestCan)
      { 
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_ACCESS", 
                          AuthCanString (rqptr->rqAuth.RequestCan,
                                         AUTH_CAN_FORMAT_LONG),
                          VarType)))
            return (status);
      }

      if (rqptr->rqAuth.PathParameterPtr &&
          rqptr->rqAuth.PathParameterPtr[0])
      { 
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_AGENT",
                          rqptr->rqAuth.PathParameterPtr, VarType)))
            return (status);
      }

      if (rqptr->rqAuth.GroupWritePtr &&
          rqptr->rqAuth.GroupWritePtr[0] &&
          VMSok(rqptr->rqAuth.GroupWriteStatus))
      {
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_GROUP",
                          rqptr->rqAuth.GroupWritePtr, VarType)))
            return (status);
      }
      else
      if (rqptr->rqAuth.GroupReadPtr &&
          rqptr->rqAuth.GroupReadPtr[0] &&
          VMSok(rqptr->rqAuth.GroupReadStatus))
      {
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_GROUP",
                          rqptr->rqAuth.GroupReadPtr, VarType)))
            return (status);
      }

      if (rqptr->AgentRequestPtr &&
          rqptr->rqAuth.PathLocationPtr)
      {
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_PATH",
                          rqptr->rqAuth.PathLocationPtr, VarType)))
            return (status);
      }

      if ((rqptr->RemoteUserPassword[0] &&
           rqptr->rqAuth.PathParameterPtr &&
           rqptr->rqAuth.PathParameterPtr[0]) ||
          rqptr->rqAuth.SourceRealm == AUTH_SOURCE_EXTERNAL)
      {
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_PASSWORD",
                          rqptr->RemoteUserPassword, VarType)))
            return (status);
      }

      if (VMSnok (status =
          CgiVariable (rqptr, "AUTH_REALM", rqptr->rqAuth.RealmPtr, VarType)))
         return (status);

      if (VMSnok (status =
          CgiVariable (rqptr, "AUTH_REALM_DESCRIPTION",
                       rqptr->rqAuth.RealmDescrPtr, VarType)))
         return (status);

      if (rqptr->rqAuth.RemoteUser[0])
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_REMOTE_USER",
                          rqptr->rqAuth.RemoteUser, VarType)))
            return (status);

      if (rqptr->rqAuth.UserDetailsPtr &&
          rqptr->rqAuth.UserDetailsPtr[0])
      {
         if (VMSnok (status =
             CgiVariable (rqptr, "AUTH_USER",
                          rqptr->rqAuth.UserDetailsPtr, VarType)))
            return (status);
      }
   }

   /* if there is an associated request body and it's being decoded */
   if (rqptr->rqHeader.Method == HTTP_METHOD_POST)
      BodyUnEncodeStream = BodyReadUnEncode (rqptr);
   else
      BodyUnEncodeStream = false;

   if (BodyUnEncodeStream)
   {
      /* indicate the content-length is unknown because of the decode */
      if (VMSnok (status =
          CgiVariable (rqptr, "CONTENT_LENGTH", "?", VarType)))
         return (status);
   }
   else
   {
      sys$fao (&Number64FaoDsc, 0, &StringDsc,
               &rqptr->rqHeader.ContentLength64);
      if (VMSnok (status =
          CgiVariable (rqptr, "CONTENT_LENGTH", String, VarType)))
         return (status);
   }

   if (rqptr->rqHeader.ContentTypePtr)
   {
      if (VMSnok (status =
          CgiVariable (rqptr, "CONTENT_TYPE",
                       rqptr->rqHeader.ContentTypePtr, VarType)))
         return (status);
   }
   else
   if (rqptr->rqContentInfo.ContentTypePtr &&
       !strsame (rqptr->rqContentInfo.ContentTypePtr, "x-internal/", 11))
   {
      if (VMSnok (status =
          CgiVariable (rqptr, "CONTENT_TYPE",
                       rqptr->rqContentInfo.ContentTypePtr, VarType)))
         return (status);
   }
   else
   {
      if (VMSnok (status =
          CgiVariable (rqptr, "CONTENT_TYPE", "", VarType)))
         return (status);
   }

   if (!rqptr->rqPathSet.ResponseCspLength ||
       !isalpha(*rqptr->rqPathSet.ResponseCspPtr))
   {
      /* if a nonce does not exist (likely) then use an ephemeral one */
      if (!*(cptr = rqptr->CspNonce))
      {
         cptr = NULL;
         GenerateCspNonce (rqptr);
      }
      status = CgiVariable (rqptr, "CSP_NONCE", rqptr->CspNonce, VarType);
      if (!cptr) rqptr->CspNonce[0] = '\0';
      if (VMSnok (status)) return (status);
   }

   if (!(cptr = rqptr->rqPathSet.MapRootPtr)) cptr = "";
   if (VMSnok (status =
       CgiVariable (rqptr, "DOCUMENT_ROOT", cptr, VarType)))
      return (status);

   if (Config.cfScript.GatewayBg &&
       rqptr->ServicePtr->RequestScheme == SCHEME_HTTP)
   {
      if (*(cptr = rqptr->NetIoPtr->BgDevName))
         if (VMSnok (status =
            CgiVariable (rqptr, "GATEWAY_BG",
                         cptr == '_' ? cptr+1 : cptr,
                         VarType)))
           return (status);
   }

   if (VMSnok (status =
       CgiVariable (rqptr, "GATEWAY_INTERFACE", "CGI/1.1", VarType)))
      return (status);

   if (VarType == CGI_VARIABLE_STREAM)
   {
      /* if 'stream' then must be CGIplus */
      if (rqptr->rqCgi.EofLength)
         if (VMSnok (status =
             CgiVariable (rqptr, "GATEWAY_EOF", rqptr->rqCgi.EofStr, VarType)))
            return (status);
   }

   if (rqptr->rqCgi.EotLength)
      if (VMSnok (status =
          CgiVariable (rqptr, "GATEWAY_EOT", rqptr->rqCgi.EotStr, VarType)))
         return (status);

   if (rqptr->rqCgi.EscLength)
      if (VMSnok (status =
          CgiVariable (rqptr, "GATEWAY_ESC", rqptr->rqCgi.EscStr, VarType)))
         return (status);

   if (rqptr->DclTaskPtr)
   {
      /* size of mailbox */
      sys$fao (&NumberFaoDsc, 0, &StringDsc, DclSysOutputSize);
      if (VMSnok (status =
           CgiVariable (rqptr, "GATEWAY_MRS", String, VarType)))
          return (status);
   }

   sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->NetIoPtr->TcpMaxQio);
   if (VMSnok (status =
        CgiVariable (rqptr, "GATEWAY_QIO", String, VarType)))
       return (status);

   /*************/
   /* SET html= */
   /*************/

   if (rqptr->rqPathSet.HtmlBodyTagPtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "HTML_BODYTAG",
                       rqptr->rqPathSet.HtmlBodyTagPtr, VarType)))
         return (status);

   if (rqptr->rqPathSet.HtmlFooterPtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "HTML_FOOTER",
                       rqptr->rqPathSet.HtmlFooterPtr, VarType)))
         return (status);

   if (rqptr->rqPathSet.HtmlFooterTagPtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "HTML_FOOTERTAG",
                       rqptr->rqPathSet.HtmlFooterTagPtr, VarType)))
         return (status);

   if (rqptr->rqPathSet.HtmlHeaderPtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "HTML_HEADER",
                       rqptr->rqPathSet.HtmlHeaderPtr, VarType)))
         return (status);

   if (rqptr->rqPathSet.HtmlHeaderTagPtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "HTML_HEADERTAG",
                       rqptr->rqPathSet.HtmlHeaderTagPtr, VarType)))
         return (status);

   /**********************************/
   /* request header field variables */
   /**********************************/

   memcpy (FieldName, "HTTP_", 5);
   zptr = FieldName + sizeof(FieldName);
   DictIterate (rqptr->rqDictPtr, NULL);
   while (denptr = DictIterate (rqptr->rqDictPtr, DICT_TYPE_REQUEST))
   {
      cptr = DICT_GET_KEY(denptr);

      /* these are not supplied for one reason or another */
      if (MATCH14 (cptr, "authorization") &&
          rqptr->rqAuth.SourceRealm != AUTH_SOURCE_AGENT_OPAQUE &&
          rqptr->rqAuth.SourceRealm != AUTH_SOURCE_EXTERNAL &&
          rqptr->rqAuth.SourceRealm != AUTH_SOURCE_OPAQUE)
         continue;

      if (MATCH4(cptr,"prox"))
      {
         if (MATCH20 (cptr, "proxy-authorization")) continue;
         /* mitigate httpoxy vulnerability by introducing an extra underscore */
         if (MATCH6 (cptr, "proxy")) cptr = "_PROXY";
      }

      sptr = FieldName + 5;
      while (*cptr && sptr < zptr)
      {
         if (isalnum(*cptr))
            *sptr++ = to_upper(*cptr);
         else
         if (!ISLWS(*cptr))
            *sptr++ = '_';
         cptr++;
      }
      if (sptr >= zptr)
      {
         ErrorVmsStatus (rqptr, SS$_RESULTOVF, FI_LI);
         return (STS$K_ERROR);
      }
      *sptr = '\0';

      cptr = DICT_GET_VALUE(denptr);

      if (VMSnok (status = CgiVariable (rqptr, FieldName, cptr, VarType)))
         return (status);
   }

   /*********************/
   /* rest of variables */
   /*********************/

   if (VMSnok (status =
       CgiVariable (rqptr, "PATH_INFO", rqptr->rqHeader.PathInfoPtr, VarType)))
      return (status);

   switch (rqptr->PathOds)
   {
      case 0 : cptr = NULL; break;
      case MAPURL_PATH_ODS_2 : cptr = "2"; break;
      case MAPURL_PATH_ODS_5 : if (OdsExtended) cptr = "5"; break;
      case MAPURL_PATH_ODS_ADS : cptr = "ADS"; break;
      case MAPURL_PATH_ODS_PWK : cptr = "PWK"; break;
      case MAPURL_PATH_ODS_SMB : cptr = "SMB"; break;
      case MAPURL_PATH_ODS_SRI : cptr = "SRI"; break;
      default : cptr = "?";
   }
   if (cptr)
      if (VMSnok (status =
          CgiVariable (rqptr, "PATH_ODS", cptr, VarType)))
         return (status);

   cptr = rqptr->ParseOds.ExpFileName;
   if (*cptr && rqptr->rqPathSet.ScriptSyntaxUnix)
   {
      if (VMSnok (status = MapOdsVmsToUnix (cptr, String, sizeof(String))))
      {
         ErrorVmsStatus (rqptr, status, FI_LI);
         return (status);
      }
      cptr = String;
   }

   if (VMSnok (status =
       CgiVariable (rqptr, "PATH_TRANSLATED", cptr, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "QUERY_STRING",
                    rqptr->rqHeader.QueryStringPtr, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REMOTE_ADDR",
                    rqptr->ClientPtr->IpAddressString, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REMOTE_HOST",
                    rqptr->ClientPtr->Lookup.HostName, VarType)))
      return (status);

   /* if client address has been rewritten the port is no longer reliable */
   if (rqptr->ClientPtr->SetClientAddress)
      SET2(String,'0\0');
   else
      sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->ClientPtr->IpPort);
   if (VMSnok (status =
       CgiVariable (rqptr, "REMOTE_PORT", String, VarType)))
      return (status);

   if (rqptr->rqAuth.CaseLess)
   {
      /* force remote user name to upper-case if a case-less authentication */
      zptr = (sptr = String) + sizeof(String)-1;
      for (cptr = rqptr->RemoteUser;
           *cptr && sptr < zptr;
           *sptr++ = to_upper(*cptr++));
      *sptr = '\0';
      if (VMSnok (status =
          CgiVariable (rqptr, "REMOTE_USER", String, VarType)))
         return (status);
   }
   else
   if (VMSnok (status =
       CgiVariable (rqptr, "REMOTE_USER", rqptr->RemoteUser, VarType)))
      return (status);

   if (rqptr->rqPathSet.CharsetPtr &&
       rqptr->rqPathSet.CharsetPtr[0] &&
       rqptr->rqPathSet.CharsetPtr[0] != '(')
   {
      if (VMSnok (status =
          CgiVariable (rqptr, "REQUEST_CHARSET",
                       rqptr->rqPathSet.CharsetPtr, VarType)))
         return (status);
   }

   if (rqptr->rqPathSet.ContentTypePtr)
   {
      if (VMSnok (status =
          CgiVariable (rqptr, "REQUEST_CONTENT_TYPE",
                       rqptr->rqPathSet.ContentTypePtr, VarType)))
         return (status);
   }

   if ((rqptr->AgentRequestPtr &&
        rqptr->AgentRequestPtr[0]) ||
       (rqptr->rqAuth.PathParameterPtr &&
        rqptr->rqAuth.PathParameterPtr[0]))
   {
      /* AUTHAGENT hangs when called for a POST request (per JPP) */
      if (VMSnok (status =
          CgiVariable (rqptr, "REQUEST_METHOD", "GET", VarType)))
         return (status);
   }
   else
   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_METHOD",
                    rqptr->rqHeader.MethodName, VarType)))
      return (status);

   if (HTTP2_REQUEST(rqptr))
      cptr = "HTTP/2";
   else
   if (rqptr->rqHeader.HttpVersion == HTTP_VERSION_1_1)
      cptr = "HTTP/1.1";
   else
   if (rqptr->rqHeader.HttpVersion == HTTP_VERSION_1_0)
      cptr = "HTTP/1.0";
   else
      cptr = "HTTP/0.9";
   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_PROTOCOL", cptr, VarType)))
      return (status);

   if (HTTP2_REQUEST(rqptr))
      if (rqptr->Http2Ptr->PingMicroSeconds)
         if (denptr = DictLookup (rqptr->rqDictPtr, DICT_TYPE_INTERNAL,
                                  "http2_ping", 10))
            if (VMSnok (status =
                CgiVariable (rqptr, "HTTP2_PING",
                             DICT_GET_VALUE(denptr), VarType)))
               return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_SCHEME",
                    rqptr->ServicePtr->RequestSchemeNamePtr, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_TIME_GMT",
                    rqptr->rqTime.GmDateTime, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_TIME_LOCAL", LocalDateTime, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "REQUEST_URI",
                    rqptr->rqHeader.RequestUriPtr, VarType)))
      return (status);

   /* Apache-like */
   if (rqptr->rqCgi.ScriptFileNamePtr &&
       rqptr->rqCgi.ScriptFileNamePtr[0])
   {
      cptr = rqptr->rqCgi.ScriptFileNamePtr;
      if (rqptr->rqPathSet.ScriptSyntaxUnix)
      {
         if (VMSnok (status = MapOdsVmsToUnix (cptr, String, sizeof(String))))
         {
            ErrorVmsStatus (rqptr, status, FI_LI);
            return (status);
         }
         cptr = String;
      }

      if (VMSnok (status =
          CgiVariable (rqptr, "SCRIPT_FILENAME", cptr, VarType)))
         return (status);
   }

   if (rqptr->rqPathSet.ScriptDefaultPtr &&
       rqptr->rqPathSet.ScriptDefaultPtr[0] != '#')
   {
      /* script default directory ('#' means backward-compatible) */
      if (VMSnok (status =
           CgiVariable (rqptr, "SCRIPT_DEFAULT",
                        rqptr->rqPathSet.ScriptDefaultPtr, VarType)))
          return (status);
   }

   /* if it's "/" (empty from mapping) then make it "" (really empty :-) */
   if (VMSnok (status =
       CgiVariable (rqptr, "SCRIPT_NAME",
                    rqptr->ScriptName[0] && rqptr->ScriptName[1] ?
                      rqptr->ScriptName : "",
                    VarType)))
      return (status);

   if (rqptr->DclTaskPtr &&
       rqptr->DclTaskPtr->TaskType == DCL_TASK_TYPE_RTE_SCRIPT &&
       rqptr->DclTaskPtr->ScriptRunTime[0])
   {
      /* Run-Time Environment (RTE engine file name) */
      if (VMSnok (status =
           CgiVariable (rqptr, "SCRIPT_RTE",
                        rqptr->DclTaskPtr->ScriptRunTime, VarType)))
          return (status);
   }

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_ADDR",
                    rqptr->ServicePtr->ServerIpAddressString, VarType)))
      return (status);

   /* Apache-like */
   if (Config.cfServer.AdminEmail[0])
      if (VMSnok (status =
          CgiVariable (rqptr, "SERVER_ADMIN",
                       Config.cfServer.AdminEmail, VarType)))
         return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_CHARSET",
                    Config.cfContent.CharsetDefault, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_GMT", TimeGmtString, VarType)))
      return (status);

   if (rqptr->ClientPtr->MultiHomeIpAddressString[0])
      if (VMSnok (status =
          CgiVariable (rqptr, "SERVER_MULTIHOME",
                       rqptr->ClientPtr->MultiHomeIpAddressString, VarType)))
         return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_NAME",
                    rqptr->ServicePtr->ServerHostName, VarType)))
      return (status);

   if (rqptr->rqResponse.HttpVersion == HTTP_VERSION_1_1)
      cptr = "HTTP/1.1";
   else
   if (rqptr->rqResponse.HttpVersion == HTTP_VERSION_1_0)
      cptr = "HTTP/1.0";
   else
      cptr = "HTTP/0.9";
   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_PROTOCOL", cptr, VarType)))
      return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_PORT",
                    rqptr->ServicePtr->ServerPortString, VarType)))
      return (status);

   /* Apache-like */
   ServerSignature (rqptr, String, sizeof(String));
   if (String[0])
      if (VMSnok (status =
          CgiVariable (rqptr, "SERVER_SIGNATURE", String, VarType)))
         return (status);

   if (VMSnok (status =
       CgiVariable (rqptr, "SERVER_SOFTWARE", SoftwareID, VarType)))
      return (status);

   /* Apache-like */
   if (VMSnok (status =
       CgiVariable (rqptr, "UNIQUE_ID", GenerateUniqueId(rqptr), VarType)))
      return (status);

   /* if the client address has been rewritten */
   if (rqptr->ClientPtr->SetClientAddress)
      if (VMSnok (status =
          CgiVariable (rqptr, "UPSTREAM_ADDR",
                       rqptr->ClientPtr->UpstreamIpAddressString,
                       VarType)))
         return (status);

   if (rqptr->RawSocketRequest)
   {
      if (rqptr->rqWebSocket.InputChannel)
      {
         if (VMSnok (status = CgiVariable (rqptr, "RAWSOCKET_INPUT",
                     rqptr->rqWebSocket.InputDevName, VarType)))
            return (status);
         sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->rqWebSocket.InputSize);
         if (VMSnok (status = CgiVariable (rqptr, "RAWSOCKET_INPUT_MRS",
                     String, VarType)))
            return (status);
      }

      if (rqptr->rqWebSocket.OutputChannel)
      {
         if (VMSnok (status = CgiVariable (rqptr, "RAWSOCKET_OUTPUT",
                     rqptr->rqWebSocket.OutputDevName, VarType)))
            return (status);
         sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->rqWebSocket.OutputSize);
         if (VMSnok (status = CgiVariable (rqptr, "RAWSOCKET_OUTPUT_MRS",
                     String, VarType)))
            return (status);
      }
   }
   else
   if (rqptr->WebSocketRequest)
   {
      sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->rqHeader.WebSocketVersion);
      if (VMSnok (status = CgiVariable (rqptr, "WEBSOCKET_VERSION",
                  String, VarType)))
         return (status);

      if (rqptr->rqWebSocket.InputChannel)
      {
         if (VMSnok (status = CgiVariable (rqptr, "WEBSOCKET_INPUT",
                     rqptr->rqWebSocket.InputDevName, VarType)))
            return (status);
         sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->rqWebSocket.InputSize);
         if (VMSnok (status = CgiVariable (rqptr, "WEBSOCKET_INPUT_MRS",
                     String, VarType)))
            return (status);
      }

      if (rqptr->rqWebSocket.OutputChannel)
      {
         if (VMSnok (status = CgiVariable (rqptr, "WEBSOCKET_OUTPUT",
                     rqptr->rqWebSocket.OutputDevName, VarType)))
            return (status);
         sys$fao (&NumberFaoDsc, 0, &StringDsc, rqptr->rqWebSocket.OutputSize);
         if (VMSnok (status = CgiVariable (rqptr, "WEBSOCKET_OUTPUT_MRS",
                     String, VarType)))
            return (status);
      }
   }

   /* if we're WATCHing you - say something! */
   if (WATCHING (rqptr, WATCH_SCRIPT))
      if (VMSnok (status =
          CgiVariable (rqptr, "WATCH_SCRIPT", "TRUE", VarType)))
         return (status);

   /*****************/
   /* SSL variables */
   /*****************/

   if (VMSnok (status = SesolaCgiGenerateVariables (rqptr, VarType)))
      return (status);

   /***************************/
   /* query string components */
   /***************************/

   if (rqptr->rqHeader.QueryStringLength &&
       !rqptr->rqPathSet.ScriptQueryNone)
   {
      EmptyFieldNameCount = KeyCount = 0;

      cptr = rqptr->rqHeader.QueryStringPtr;
      while (*cptr && *cptr != '=') cptr++;
      /* if an equal symbol was found then its a form not a keyword search */
      if (*cptr)
      {
         /***************/
         /* form fields */
         /***************/

         memcpy (FieldName, "FORM_", 5);
         cptr = rqptr->rqHeader.QueryStringPtr;
         while (*cptr)
         {
            sptr = FieldName + 5;
            zptr = FieldName + sizeof(FieldName);
            while (*cptr && *cptr != '=' && *cptr != '&' && sptr < zptr)
            {
               if (isalnum(*cptr))
                  *sptr++ = to_upper(*cptr++);
               else
               {
                  *sptr++ = '_';
                  cptr++;
               }
            }
            if (sptr >= zptr)
            {
               ErrorVmsStatus (rqptr, SS$_RESULTOVF, FI_LI);
               return (STS$K_ERROR);
            }
            *sptr = '\0';

            if (!FieldName[5] || (*cptr && *cptr != '=' && *cptr != '&'))
            {
               if (*cptr == '&')
               {
                  /* allow back-to-back '&' (effectively null field name) */
                  cptr++;
                  continue;
               }
               /* out-of-place '=' (no field name) */
               if (rqptr->rqPathSet.ScriptQueryRelaxed)
               {
                  /* let's be relaxed about this, script will handle it */
                  sys$fao (&NumberFaoDsc, 0, &StringDsc, ++EmptyFieldNameCount);
                  FieldName[5] = '_';
                  strzcpy (FieldName+6, String, sizeof(FieldName)-6);
               }
               else
               {
                  rqptr->rqResponse.HttpStatus = 400;
                  ErrorGeneral (rqptr, MsgFor(rqptr,MSG_REQUEST_URL_FORM),
                                FI_LI);
                  return (STS$K_ERROR);
               }
            }

            /* if encountered an '=' */
            if (*cptr == '=') cptr++;
            Count = CgiUrlDecodeString (rqptr, cptr,
                                        String, sizeof(String), '&');
            if (Count < 0) return (STS$K_ERROR);
            cptr += Count;
            if (VMSnok (status =
                CgiVariable (rqptr, FieldName, String, VarType)))
               return (status);
         }

         if (VMSnok (status =
             CgiVariable (rqptr, "KEY_COUNT", "0", VarType)))
            return (status);
      }
      else
      {
         /******************/
         /* query keywords */
         /******************/

         cptr = rqptr->rqHeader.QueryStringPtr;
         while (*cptr)
         {
            sys$fao (&NumberFaoDsc, 0, &StringDsc, ++KeyCount);
            strzcpy (FieldName, "KEY_", sizeof(FieldName));
            strzcat (FieldName, String, sizeof(FieldName));

            Count = CgiUrlDecodeString (rqptr, cptr,
                                        String, sizeof(String), '+');
            if (Count < 0) return (STS$K_ERROR);
            cptr += Count;
            if (VMSnok (status =
                CgiVariable (rqptr, FieldName, String, VarType)))
               return (status);
         }

         sys$fao (&NumberFaoDsc, 0, &StringDsc, KeyCount);
         if (VMSnok (status =
             CgiVariable (rqptr, "KEY_COUNT", String, VarType)))
            return (status);
      }
   }
   else
   if (!rqptr->rqPathSet.ScriptQueryNone)
   {
      /* no keywords if no query string! */
      if (VMSnok (status =
          CgiVariable (rqptr, "KEY_COUNT", "0", VarType)))
         return (status);
   }

   /*********************/
   /* SET script=params */
   /*********************/

   if (rqptr->rqPathSet.ScriptParamsPtr)
   {
      if (WATCHMOD (rqptr, WATCH_MOD_CGI))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI, "!&Z",
                    rqptr->rqPathSet.ScriptParamsPtr);

      NameValuePtr = rqptr->rqPathSet.ScriptParamsPtr;
      for (;;)
      {
         status = StringParseNameValue (&NameValuePtr, true,
                                        FieldName, sizeof(FieldName),
                                        String, sizeof(String));
         if (VMSnok (status)) break;
         status = CgiVariable (rqptr, FieldName, String, VarType);
         if (VMSnok (status)) break;
      }
      if (status != SS$_ENDOFFILE) ErrorVmsStatus (rqptr, status, FI_LI);
   }

   /******************/
   /* dictionary CGI */
   /******************/

   /* these can (also) be used to overwrite any CGI variable! */
   DictIterate (rqptr->rqDictPtr, NULL);
   while (denptr = DictIterate (rqptr->rqDictPtr, DICT_TYPE_CONFIG))
   {
      cptr = DICT_GET_KEY(denptr);
      if (!MATCH4 (cptr, "cgi_")) continue;
      sptr = DICT_GET_VALUE(denptr);
      if (VMSnok (status = CgiVariable (rqptr, cptr+4, sptr, VarType)))
         return (status);
   }

   /**************************/
   /* auth agent script-meta */
   /**************************/

   if (rqptr->rqAuth.ScriptMetaPtr)
   {
      NameValuePtr = rqptr->rqAuth.ScriptMetaPtr;
      for (;;)
      {
         status = StringParseNullNameValue (&NameValuePtr, true,
                                            FieldName, sizeof(FieldName),
                                            String, sizeof(String));
         if (VMSnok (status)) break;
         status = CgiVariable (rqptr, FieldName, String, VarType);
         if (VMSnok (status)) break;
      }
      if (status != SS$_ENDOFFILE) ErrorVmsStatus (rqptr, status, FI_LI);
   }

   /***********/
   /* finally */
   /***********/

   if (rqptr->rqCgi.SymbolTruncatePtr)
      if (VMSnok (status =
          CgiVariable (rqptr, "SERVER_TRUNCATE",
                       rqptr->rqCgi.SymbolTruncatePtr, VarType)))
         return (status);

   /*********************/
   /* DCL variable list */
   /*********************/
   
   if (rqptr->DECnetTaskPtr ||
       (rqptr->DclTaskPtr &&
        rqptr->DclTaskPtr->TaskType == DCL_TASK_TYPE_CGI_SCRIPT))
   {
      char  buf [4096];
      zptr = (sptr = buf) + sizeof(buf)-1;
      cptr = rqptr->rqCgi.BufferPtr;
      for (;;)
      {
         if (!(Count = *(USHORTPTR)cptr)) break;
         cptr += sizeof(ushort);
         NameValuePtr = cptr;
         if (sptr > buf && sptr < zptr) *sptr++ = ',';
         while (*cptr && *cptr != '=' && sptr < zptr) *sptr++ = *cptr++;
         if (sptr >= zptr) return (SS$_RESULTOVF);
         cptr = NameValuePtr + Count;
      }
      *sptr++ = '\0';

      if (VMSnok (status =
          CgiVariable (rqptr, "GATEWAY_SYMBOLS", buf, VarType)))
         return (status);
   }

   /********************/
   /* end of variables */
   /********************/

  if (WATCHING (rqptr, WATCH_CGI))
     WatchThis (WATCHITM(rqptr), WATCH_CGI, "VARIABLES !&,UL bytes",
                rqptr->rqCgi.BufferLength - rqptr->rqCgi.BufferRemaining);

   return (CgiVariable (rqptr, NULL, NULL, VarType));
}

/*****************************************************************************/
/*
URL-decode a URL-encoded string!  Return -1 if an error occurs, otherwise
return the number of characters read from the original, encoded string.
*/ 

int CgiUrlDecodeString
(
REQUEST_STRUCT *rqptr,
char *EncodedString,
char *DecodedString,
int DecodedStringSize,
char Separator
)
{
   unsigned char  ch;
   char  *cptr, *sptr, *zptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                 "CgiUrlDecodeString() !&Z", EncodedString);

   cptr = EncodedString;
   zptr = (sptr = DecodedString) + DecodedStringSize;
   while (*cptr && *cptr != Separator && sptr < zptr)
   {
      if (*cptr == '+')
      {
         *sptr++ = ' ';
         cptr++;
      }
      else
      if (*cptr == '%')
      {
         /* an escaped character ("%xx" where xx is a hex number) */
         cptr++;
         ch = 0;
         if (*cptr >= '0' && *cptr <= '9')
            { ch = (*cptr - (int)'0') << 4; cptr++; }
         else
         if (to_lower(*cptr) >= 'a' && to_lower(*cptr) <= 'f')
            { ch = (to_lower(*cptr) - (int)'a' + 10) << 4; cptr++; }
         else
         {
            rqptr->rqResponse.HttpStatus = 400;
            ErrorGeneral (rqptr, MsgFor(rqptr,MSG_REQUEST_URL_ENC), FI_LI);
            return (-1);
         }
         if (*cptr >= '0' && *cptr <= '9')
            { ch += (*cptr - (int)'0'); cptr++; }
         else
         if (to_lower(*cptr) >= 'a' && to_lower(*cptr) <= 'f')
            { ch += (to_lower(*cptr) - (int)'a' + 10); cptr++; }
         else
         {
            rqptr->rqResponse.HttpStatus = 400;
            ErrorGeneral (rqptr, MsgFor(rqptr,MSG_REQUEST_URL_ENC), FI_LI);
            return (-1);
         }
         if (sptr < zptr) *sptr++ = ch;
      }
      else
         *sptr++ = *cptr++;
   }
   if (sptr >= zptr)
   {
      ErrorGeneral (rqptr, MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
      return (-1);
   }
   *sptr = '\0';
   if (*cptr) cptr++;

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchDataFormatted ("!&Z\n", DecodedString);

   return (cptr - EncodedString);
}

/*****************************************************************************/
/*
Depending on whether the CGI variable is for creation at the DCL CLI or as part
of a CGI variable stream (e.g. CGIplus) create a null-terminated string
containing either commands to create a DCL symbol, or a 'name=value' pair.

DCL symbol creation at the command line is limited by the CLI command line 
length (255 characters for pre 7.3-2, 4095 for post 7.3-1).  Symbol values
however can be up to 1024 (or pragmatically about 1000 characters) for pre
7.3-2 and 8192 (or thereabouts) for post 7.3-1, probably enough for any CGI
variable value.  If a CGI value is too large for for a single command-line
assignment then build it up using multiple assignments, a symbol assignment
kludge!
*/ 

int CgiVariable
(
REQUEST_STRUCT *rqptr,
char *SymbolName,
char *SymbolValue,
int VarType
)
{
   static char  DeleteAllLocalSymbol[] = "DELETE/SYMBOL/LOCAL/ALL";

   int  size, status,
        CliLineSize,
        CliSymbolSize,
        SymbolCount,
        SymbolValueLength;
   unsigned short  Length;
   char  *cptr, *sptr, *zptr,
         *CgiPlusInCCPtr,
         *CgiVariablePrefixPtr;
   char  NameString [256],
         ValueString [2048];

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                 "CgiVariable() !UL !AZ !&Z", VarType, SymbolName, SymbolValue);

   if (!SymbolName)
   {
      /************************/
      /* end of CGI variables */
      /************************/

      if (rqptr->rqCgi.BufferRemaining < sizeof(short))
         CgiVariableBufferMemory (rqptr, sizeof(short));

      /* zero length record terminates generated CGI variables */
      SET2(rqptr->rqCgi.BufferCurrentPtr,0);
      rqptr->rqCgi.BufferRemaining -= sizeof(short);
      rqptr->rqCgi.BufferCurrentPtr += sizeof(short);

      return (SS$_NORMAL);
   }

   if (!SymbolValue)
   {
      /************************/
      /* parse <name>=<value> */
      /************************/

      zptr = (sptr = NameString) + sizeof(NameString)-1;
      cptr = SymbolName;
      while (*cptr && *cptr != '=' && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
      while (*cptr && *cptr != '=') cptr++;
      if (*cptr) cptr++;
      zptr = (sptr = ValueString) + sizeof(ValueString)-1;
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
      SymbolName = NameString;
      SymbolValue = ValueString;
   }

   if (rqptr->rqPathSet.CgiPrefixPtr)
      CgiVariablePrefixPtr = rqptr->rqPathSet.CgiPrefixPtr;
   else
      CgiVariablePrefixPtr = DclCgiVariablePrefix;

   CgiPlusInCCPtr = rqptr->rqPathSet.CgiPlusInCC;

   if (!SymbolValue) SymbolValue = "";

   if (WATCHING (rqptr, WATCH_CGI))
      WatchDataFormatted ("!AZ=!AZ\n", SymbolName, SymbolValue);

   if (VarType == CGI_VARIABLE_STREAM)
   {
      /******************************/
      /* variables in record stream */
      /******************************/

      if (rqptr->rqCgi.BufferRemaining <= DclCgiPlusInSize + sizeof(short))
         CgiVariableBufferMemory (rqptr, DclCgiPlusInSize*2);

      zptr = (sptr = rqptr->rqCgi.BufferCurrentPtr + sizeof(short)) +
             (rqptr->rqCgi.BufferRemaining - sizeof(short));
      for (cptr = CgiVariablePrefixPtr; *cptr; *sptr++ = *cptr++);
      for (cptr = SymbolName; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr) *sptr++ = '=';
      for (cptr = SymbolValue; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr && CgiPlusInCCPtr[0]) *sptr++ = CgiPlusInCCPtr[0];
      if (sptr < zptr && CgiPlusInCCPtr[1]) *sptr++ = CgiPlusInCCPtr[1];
      if (sptr < zptr) *sptr++ = '\0';
      if (sptr < zptr)
      {
         Length = sptr - (char*)rqptr->rqCgi.BufferCurrentPtr - sizeof(short);
         SET2(rqptr->rqCgi.BufferCurrentPtr,Length);
         rqptr->rqCgi.BufferRemaining -= Length + sizeof(short);
         rqptr->rqCgi.BufferCurrentPtr += Length + sizeof(short);
         return (SS$_NORMAL);
      }
      ErrorGeneral (rqptr, MsgFor(rqptr,MSG_GENERAL_OVERFLOW), FI_LI);
      return (STS$K_ERROR);
   }

   /********************************/
   /* variables created at DCL CLI */
   /********************************/

   if (SysInfo.VersionInteger >= 732)
   {
      /* take advantage of EDCL - ultimately limited by 'DclSysCommandSize'! */
      CliLineSize = 4095;
      CliSymbolSize = 8191;
   }
   else
   {
      /* same old command-line length we've grown up knowing and hating */
      CliLineSize = 255;
      CliSymbolSize = 1023;
   }

   if (rqptr->rqCgi.BufferRemaining <= CliLineSize + sizeof(short))
      CgiVariableBufferMemory (rqptr, DclSysCommandSize);

   /********************************/
   /* try for a single assignment! */
   /********************************/

   zptr = (sptr = rqptr->rqCgi.BufferCurrentPtr + sizeof(short)) + CliLineSize;
   for (cptr = CgiVariablePrefixPtr; *cptr; *sptr++ = *cptr++);
   for (cptr = SymbolName; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '=';
   if (sptr < zptr) *sptr++ = '=';
   if (sptr < zptr) *sptr++ = '\"';
   cptr = SymbolValue;
   while (*cptr && sptr < zptr)
   {
      if (*cptr == '\"')
      {
         /* escape double quotes, by doubling up */
         if (sptr+2 < zptr)
         {
            cptr++;
            *sptr++ = '\"';
            *sptr++ = '\"';
         }
         else
            zptr = sptr;
         continue;
      }

      if (SAME2(cptr,'\'\''))
      {
         /* escape doubled single quotes by concatenation */
         if (sptr+4 < zptr)
         {
            cptr++;
            memcpy (sptr, "\'\"+\"", 4);
            sptr += 4;
         }
         else
            zptr = sptr;
         continue;
      }

      if (sptr < zptr) *sptr++ = *cptr++;
   }
   if (sptr < zptr) *sptr++ = '\"';
   if (sptr < zptr)
   {
      /**********************************/
      /* fitted into the one assignment */
      /**********************************/

      *sptr++ = '\0';
      Length = sptr - (char*)rqptr->rqCgi.BufferCurrentPtr - sizeof(short);
      SET2(rqptr->rqCgi.BufferCurrentPtr,Length);
      rqptr->rqCgi.BufferRemaining -= Length + sizeof(short);
      rqptr->rqCgi.BufferCurrentPtr += Length + sizeof(short);
      return (SS$_NORMAL);
   }

   /************************/
   /* multiple assignments */
   /************************/

   /* plus one allows for the first equate symbol in the global assignment */
   SymbolValueLength = strlen(SymbolName) + 1;

   /* loop assigning maximum amount allowed by DCL until all assigned */
   SymbolCount = 0;
   cptr = SymbolValue;
   while (*cptr)
   {
      if (rqptr->rqCgi.BufferRemaining <= CliLineSize + sizeof(short))
         CgiVariableBufferMemory (rqptr, DclSysCommandSize);

      if (WATCHMOD (rqptr, WATCH_MOD_CGI))
         WatchDataFormatted ("!&Z\n", cptr);

      /* set this to DCL line length minus one for the terminating quote */
      zptr = (sptr = rqptr->rqCgi.BufferCurrentPtr + sizeof(short)) +
             CliLineSize - 1;

      /* allow for each of the +B+C apparently implemented as +".."+".." */
      SymbolValueLength += 3;
      if (SymbolValueLength >= CliSymbolSize) break;

      switch (++SymbolCount)
      {
         case 1 : memcpy (sptr, "A=\"", 3); break;
         case 2 : memcpy (sptr, "B=\"", 3); break;
         case 3 : memcpy (sptr, "C=\"", 3); break;
         case 4 : memcpy (sptr, "D=\"", 3); break;
         case 5 : memcpy (sptr, "E=\"", 3); break;
         case 6 : memcpy (sptr, "F=\"", 3); break;
         default : memcpy (sptr, "X=\"", 3); break;
      }
      sptr += 3;

      while (*cptr && sptr < zptr)
      {
         SymbolValueLength++;
         /* quit one before completely full to allow for trailing quote */
         if (SymbolValueLength >= CliSymbolSize) break;

         if (*cptr == '\"')
         {
            /* escape double quotes, by doubling up */
            if (sptr+2 < zptr)
            {
               cptr++;
               *sptr++ = '\"';
               *sptr++ = '\"';
            }
            else
               zptr = sptr;
            continue;
         }

         if (SAME2(cptr,'\'\''))
         {
            /* escape doubled single quotes by concatenation */
            if (sptr+4 < zptr)
            {
               cptr++;
               memcpy (sptr, "\'\"+\"", 4);
               sptr += 4;
            }
            else
               zptr = sptr;
            continue;
         }

         *sptr++ = *cptr++;
      }

      *sptr++ = '\"';
      *sptr++ = '\0';
      Length = sptr - (char*)rqptr->rqCgi.BufferCurrentPtr - sizeof(short);
      SET2(rqptr->rqCgi.BufferCurrentPtr,Length);
      rqptr->rqCgi.BufferRemaining -= Length + sizeof(short);
      rqptr->rqCgi.BufferCurrentPtr += Length + sizeof(short);
      if (SymbolValueLength >= CliSymbolSize) break;
   }

   if (SymbolValueLength >= CliSymbolSize)
   {
      /**************************/
      /* symbol value too large */
      /**************************/

      if (rqptr->rqPathSet.ScriptSymbolTruncate)
      {
         /* add this CGI variable name to the truncated symbol(s) list */
         rqptr->rqCgi.SymbolTruncatePtr =
            VmReallocHeap (rqptr,
                           rqptr->rqCgi.SymbolTruncatePtr,
                           size = rqptr->rqCgi.SymbolTruncateLength +
                                  strlen(SymbolName)+2, FI_LI);
         if (rqptr->rqCgi.SymbolTruncateLength)
         {
            strzcat (rqptr->rqCgi.SymbolTruncatePtr, ",", size);
            rqptr->rqCgi.SymbolTruncateLength++;
         }
         strzcat (rqptr->rqCgi.SymbolTruncatePtr, SymbolName, size);
         rqptr->rqCgi.SymbolTruncateLength += strlen(SymbolName);
      }
      else
      {
         /* cannot create a symbol this size */
         rqptr->rqResponse.ErrorTextPtr = SymbolName;
         rqptr->rqResponse.ErrorOtherTextPtr = ErrorCgiSymbol;
         ErrorVmsStatus (rqptr, CLI$_BUFOVF, FI_LI);
         ErrorNoticed (rqptr, CLI$_BUFOVF, "!AZ too big at !UL characters,",
                       FI_LI, SymbolName, strlen(SymbolValue));
         return (STS$K_ERROR);
      }
   }

   /* assign the temporary symbol value(s) to the CGI symbol */
   if (rqptr->rqCgi.BufferRemaining <= CliLineSize + sizeof(short))
      CgiVariableBufferMemory (rqptr, DclSysCommandSize);
   sptr = rqptr->rqCgi.BufferCurrentPtr + sizeof(short);
   for (cptr = CgiVariablePrefixPtr; *cptr; *sptr++ = *cptr++);
   for (cptr = SymbolName; *cptr; *sptr++ = *cptr++);
   switch (SymbolCount)
   {
      case 1 : memcpy (sptr, "==A", 4); sptr += 4; break;
      case 2 : memcpy (sptr, "==A+B", 6); sptr += 6; break;
      case 3 : memcpy (sptr, "==A+B+C", 8); sptr += 8; break;
      case 4 : memcpy (sptr, "==A+B+C+D", 10); sptr += 10; break;
      case 5 : memcpy (sptr, "==A+B+C+D+E", 12); sptr += 12; break;
      case 6 : memcpy (sptr, "==A+B+C+D+E+F", 14); sptr += 14; break;
      default : break;
   }
   Length = sptr - (char*)rqptr->rqCgi.BufferCurrentPtr - sizeof(short);

   SET2(rqptr->rqCgi.BufferCurrentPtr,Length);
   rqptr->rqCgi.BufferRemaining -= Length + sizeof(short);
   rqptr->rqCgi.BufferCurrentPtr += Length + sizeof(short);

   /* not really necessary, but let's be tidy */
   if (rqptr->rqCgi.BufferRemaining <=
       sizeof(DeleteAllLocalSymbol) + sizeof(short))
      CgiVariableBufferMemory (rqptr, DclSysCommandSize);
   memcpy (rqptr->rqCgi.BufferCurrentPtr + sizeof(short),
           DeleteAllLocalSymbol, sizeof(DeleteAllLocalSymbol));
   SET2(rqptr->rqCgi.BufferCurrentPtr,sizeof(DeleteAllLocalSymbol));
   rqptr->rqCgi.BufferRemaining -= sizeof(DeleteAllLocalSymbol) + sizeof(short);
   rqptr->rqCgi.BufferCurrentPtr += sizeof(DeleteAllLocalSymbol) + sizeof(short);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Allocate, or reallocate on subsequent calls, memory for storing the CGI
variables in.  Adjusts the CGI variable buffer pointers, lengths, etc,
appropriately.
*/ 

void CgiVariableBufferMemory
(
REQUEST_STRUCT *rqptr,
int BufferChunk
)
{
   int  CurrentLength,
        CurrentOffset;

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                 "CgiVariableBufferMemory() !UL !UL !UL",
                 BufferChunk, rqptr->rqCgi.BufferLength,
                 rqptr->rqCgi.BufferCurrentPtr - rqptr->rqCgi.BufferPtr);

   if (!rqptr->rqCgi.BufferLength)
   {
      /* initialize CGI variable buffer */
      rqptr->rqCgi.BufferLength = rqptr->rqCgi.BufferRemaining = BufferChunk;
      rqptr->rqCgi.BufferPtr = rqptr->rqCgi.BufferCurrentPtr =
         VmGetHeap (rqptr, BufferChunk);
      return;
   }

   CurrentLength = rqptr->rqCgi.BufferLength;
   CurrentOffset = rqptr->rqCgi.BufferCurrentPtr - rqptr->rqCgi.BufferPtr;

   rqptr->rqCgi.BufferPtr =
      VmReallocHeap (rqptr, rqptr->rqCgi.BufferPtr,
                     CurrentLength + BufferChunk, FI_LI);

   rqptr->rqCgi.BufferLength = CurrentLength + BufferChunk;
   rqptr->rqCgi.BufferRemaining += BufferChunk;
   rqptr->rqCgi.BufferCurrentPtr = rqptr->rqCgi.BufferPtr + CurrentOffset;
}

/*****************************************************************************/
/*
Process output from a CGI/CGIplus/RTE/WebSocket script (or even plain DCL
processing).  For CGI scripts check the first output from each for CGI-relevant
HTTP header lines.  For all output check that carriage-control is as required. 
Returns one of two things.   Either a constant value ("CGI_OUTPUT_...") that
indicates some event in output processing, or a value indicating quantity of
output still available in the stream.
*/ 

int CgiOutput
(
REQUEST_STRUCT *rqptr,
char *OutputPtr,
int OutputCount
)
{
   BOOL  CgiCompliant,
         EndOfHeader,
         HTTP1dot1;
   int  cnt, idx, status,
        AbsorbCount,
        HeaderLength,
        HeaderRemaining,
        HttpStatus,
        LineCount;
   char  *cptr, *lptr, *sptr, *zptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
   {
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                 "CgiOutput() !SL !&X", OutputCount, OutputPtr);
      WatchDataDump (OutputPtr, OutputCount);
   }

   cptr = OutputPtr;
   cnt = OutputCount;

   if (!cptr)
   {
      /********************/
      /* reset CGI output */
      /********************/

      rqptr->rqCgi.CalloutInProgress =
         rqptr->rqCgi.ContentTypeText =
         rqptr->rqCgi.Header100Continue =
         rqptr->rqCgi.Header100ContinueDone =
         rqptr->rqCgi.ProcessingBody = false;

      rqptr->rqCgi.CalloutOutputCount =
         rqptr->rqCgi.OutputMode =
         rqptr->rqCgi.RecordCount =
         rqptr->rqCgi.XVMSRecordMode = 0;

      rqptr->rqCgi.ContentLength = -1;

      if (rqptr->rqCgi.HeaderPtr)
      {
         VmFreeFromHeap (rqptr, rqptr->rqCgi.HeaderPtr, FI_LI);
         rqptr->rqCgi.HeaderPtr = NULL;
      }

      rqptr->rqCgi.HeaderLength = rqptr->rqCgi.HeaderLineCount = 0;
      rqptr->rqCgi.HeaderPtr = VmGetHeap (rqptr, DclCgiHeaderSize);

      return (0);
   }

   if (rqptr->rqCgi.EofLength)
   {
      if (cnt >= rqptr->rqCgi.EofLength &&
          cnt <= rqptr->rqCgi.EofLength+2 &&
          MATCH0 (cptr, rqptr->rqCgi.EofStr, rqptr->rqCgi.EofLength))
      {
         /******************************/
         /* end of output from script! */
         /******************************/

         rqptr->rqCgi.AbsorbHeader =
            rqptr->rqCgi.BufferRecords = false;

         if (!rqptr->rqCgi.IsCliDcl &&
             !rqptr->AgentRequestPtr &&
             !rqptr->rqCgi.ProcessingBody &&
             /* ->HttpStatus can be set by DCL callout GATEWAY-BEGIN: */
             !rqptr->rqResponse.HttpStatus &&
             !rqptr->rqResponse.RedactBufferPtr &&
             !rqptr->rqResponse.RedactBufferCount &&
             Config.cfScript.CgiStrictOutput)
         {
            /* hmmmm, didn't receive any output */
            if (WATCHING (rqptr, WATCH_CGI))
               WatchThis (WATCHITM(rqptr), WATCH_CGI, ErrorCgiNotStrict);
            ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
            return (CGI_OUTPUT_NOT_STRICT);
         }

         if (rqptr->rqCache.LoadFromCgi)
         {
            rqptr->rqCache.LoadStatus = SS$_ENDOFFILE;
            CacheLoadEnd (rqptr);
         }

         return (CGI_OUTPUT_END);
      }
   }

   if (rqptr->rqCgi.CalloutInProgress)
   {
      /********************************/
      /* output escape is in progress */
      /********************************/

      if (cnt >= rqptr->rqCgi.EotLength &&
          cnt <= rqptr->rqCgi.EotLength+2 &&
          MATCH0 (cptr, rqptr->rqCgi.EotStr, rqptr->rqCgi.EotLength))
      {
         /******************************/
         /* end of escape from script! */
         /******************************/

         return (CGI_OUTPUT_ESCAPE_END);
      }

      return (CGI_OUTPUT_ESCAPE);
   }

   if (!rqptr->rqCgi.IsCliDcl &&
       rqptr->rqCgi.EscLength)
   {
      if (cnt >= rqptr->rqCgi.EscLength &&
          cnt <= rqptr->rqCgi.EscLength+2 &&
          MATCH0 (cptr, rqptr->rqCgi.EscStr, rqptr->rqCgi.EscLength))
      {
         /******************************/
         /* escape from script output! */
         /******************************/

         return (CGI_OUTPUT_ESCAPE_BEGIN);
      }
   }

   rqptr->rqCgi.RecordCount++;

   if (!rqptr->rqCgi.ProcessingBody &&
       !rqptr->rqCgi.IsCliDcl)
   {
      /******************************/
      /* processing response header */
      /******************************/

      /* buffer part of, one, or multiple lines of script (header) output */
      LineCount = 0;
      EndOfHeader = false;
      sptr = rqptr->rqCgi.HeaderPtr + rqptr->rqCgi.HeaderLength;
      zptr = sptr + DclCgiHeaderSize;
      while (*cptr && sptr < zptr)
      {
         if (*cptr != '\n' &&
             !SAME2(cptr,'\r\n') &&
             !SAME2(cptr,'\r\0'))
         {
            *sptr++ = *cptr++;
            continue;
         }
         if (*cptr == '\n')
         {
            /* record terminated by a trailing newline */
            if (sptr > rqptr->rqCgi.HeaderPtr && *(sptr-1) == '\n')
            {
               cptr++;
               EndOfHeader = true;
               break;
            }
            *sptr++ = '\r';
            if (sptr < zptr) *sptr++ = *cptr++;
            LineCount++;
            rqptr->rqCgi.HeaderCarRet = false;
            continue;
         }
         else
         if (SAME2(cptr,'\r\n'))
         {
            /* record 'correctly' terminated */
            if (sptr > rqptr->rqCgi.HeaderPtr && *(sptr-1) == '\n')
            {
               cptr++;
               cptr++;
               EndOfHeader = true;
               break;
            }
            *sptr++ = *cptr++;
            if (sptr < zptr) *sptr++ = *cptr++;
            LineCount++;
            rqptr->rqCgi.HeaderCarRet = false;
            continue;
         }
         else
         {
            /* single record terminated by a trailing carriage-return */
            if (sptr > rqptr->rqCgi.HeaderPtr && *(sptr-1) == '\n')
            {
               cptr++;
               EndOfHeader = true;
               break;
            }
            *sptr++ = *cptr++;
            if (sptr < zptr) *sptr++ = '\n';
            LineCount++;
            rqptr->rqCgi.HeaderCarRet = false;
            continue;
         }
      }

      /* an empty record can only mean one thing, a '\n' in record-mode! */
      if (!OutputCount || rqptr->rqCgi.HeaderCarRet)
      {
         /* header output contains no explicit carriage-control */
         LineCount++;
         if (sptr > rqptr->rqCgi.HeaderPtr && *(sptr-1) == '\n')
            EndOfHeader = true;
         else
         {
            if (sptr < zptr) *sptr++ = '\r';
            if (sptr < zptr) *sptr++ = '\n';
         }
      }

      if (sptr >= zptr)
      {
         if (WATCHING (rqptr, WATCH_CGI))
            WatchThis (WATCHITM(rqptr), WATCH_CGI, ErrorCgiNotStrict);
         ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
         return (CGI_OUTPUT_NOT_STRICT);
      }
      *sptr = '\0';
      HeaderLength = sptr - rqptr->rqCgi.HeaderPtr;

      if (WATCHMOD (rqptr, WATCH_MOD_CGI))
         WatchDataDump(rqptr->rqCgi.HeaderPtr, HeaderLength);
   }

   if (!rqptr->rqCgi.ProcessingBody &&
       !rqptr->rqCgi.IsCliDcl &&
       !rqptr->rqCgi.HeaderLineCount)
   {
      /****************/
      /* first record */
      /****************/

      lptr = rqptr->rqCgi.HeaderPtr;

      if (MATCH7 (lptr, "HTTP/1."))
      {
         HTTP1dot1 = (*(lptr += 7) == '1');
         for (lptr++; ISLWS(*lptr) && NOTEOL(*lptr); lptr++);

         /* get (what should be) the response status code */
         HttpStatus = atoi(lptr);

         if (HTTP1dot1 && HttpStatus == 100)
         {
            /****************/
            /* 100 continue */
            /****************/

            if (rqptr->rqCgi.Header100ContinueDone)
            {
               /* hmmm, not putting up with two of these back-to-back! */
               ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
               return (CGI_OUTPUT_NOT_STRICT);
            }
            rqptr->rqCgi.Header100Continue = true;
         }
         else
         if (HTTP1dot1 && HttpStatus == 101 &&
             (rqptr->WebSocketRequest || rqptr->RawSocketRequest))
         {
            /***************************/
            /* 101 switching protocols */
            /***************************/

            /* we'll be generating a Web Socket specific response header */
            rqptr->rqCgi.Header101Switching = rqptr->rqCgi.AbsorbHeader = true;
         }
         else
         {
            /**************************/
            /* full HTTP stream (NPH) */
            /**************************/

            if (WATCHING (rqptr, WATCH_CGI))
               WatchThis (WATCHITM(rqptr), WATCH_CGI, "RESPONSE NPH mode!AZ",
                          HTTP2_REQUEST(rqptr) ? " (but HTTP/2)" : "");

            /* status used for logging purposes */
            rqptr->rqResponse.HttpStatus = HttpStatus;
            if (!rqptr->rqCgi.AbsorbHeader)
            {
               if (WATCH_NOT_ONE_SHOT(Watch.Category))
                  WatchFilterHttpStatus (rqptr);

               if (rqptr->rqPathSet.CacheNPH &&
                   !rqptr->rqCache.LoadCheck)
               {
                  /* script output to be cached */
                  rqptr->rqCache.LoadFromCgi = CacheLoadBegin (rqptr, 0, NULL);
                  if (rqptr->rqCache.LoadFromCgi)
                     CacheLoadData (rqptr, OutputPtr, OutputCount);
               } 

               /* HTTP/2 requires an explicit response header */
               if (!HTTP2_REQUEST(rqptr))
               {
                  /* HTTP/1.n does not */
                  rqptr->rqCgi.ProcessingBody = true;
                  return (OutputCount);
               }
            }
         }
      }
      else
      {
         /************************************/
         /* check for CGI-like response line */
         /************************************/

         /* ensure it looks something like "blah-blah:" */
         while (*lptr && *lptr != ':' && !ISLWS(*lptr)) lptr++;
         if (*lptr != ':')
         {
            /***********************/
            /* non-CGI/NPH output? */
            /***********************/

            if (rqptr->AgentRequestPtr)
            {
               /* not allowed for an agent */
               if (WATCHING (rqptr, WATCH_CGI))
                  WatchThis (WATCHITM(rqptr), WATCH_CGI, ErrorCgiNotStrict);
               ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
               return (CGI_OUTPUT_NOT_STRICT);
            }

            if (Config.cfScript.CgiStrictOutput)
            {
               if (WATCHING (rqptr, WATCH_CGI))
                  WatchThis (WATCHITM(rqptr), WATCH_CGI, ErrorCgiNotStrict);
               ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
               return (CGI_OUTPUT_NOT_STRICT);
            }

            lptr = rqptr->rqCgi.HeaderPtr;
            if (*lptr == '%')
            {
               /* check for something like "%DCL-E-OPENIN, blah" */
               for (cptr = lptr+1; *cptr && isalpha(*cptr); cptr++);
               if (cptr[0] == '-' &&
                   (cptr[1] == 'F' || cptr[1] == 'E' || cptr[1] == 'W') &&
                   cptr[2] == '-')
               {
                  cptr += 3;
                  while (*cptr && isalpha(*cptr)) cptr++;
                  if (*cptr == ',')
                  {
                     /* yep, looks like it */
                     if (WATCHING (rqptr, WATCH_CGI))
                        WatchThis (WATCHITM(rqptr), WATCH_CGI,
                                   ErrorCgiCli, lptr);
                     rqptr->rqResponse.HttpStatus = 502;
                     ErrorGeneral (rqptr, lptr, FI_LI);
                     ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
                     return (CGI_OUTPUT_NOT_STRICT);
                  }
               }
            }

            ResponseHeader (rqptr, 200, "text/plain", -1, NULL, NULL);
            rqptr->rqCgi.ContentTypeText =
               rqptr->rqCgi.ProcessingBody = true;
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
         }
      }

      if (!LineCount && rqptr->rqCgi.RecordCount == 1)
      {
         /* no carriage-control has been detected in first record */
         rqptr->rqCgi.HeaderCarRet = true;
         if (sptr < zptr) *sptr++ = '\r';
         if (sptr < zptr) *sptr++ = '\n';
         LineCount++;
      }
   }

   if (!rqptr->rqCgi.ProcessingBody &&
       !rqptr->rqCgi.IsCliDcl)
   {
      /***************************/
      /* (still) response header */
      /***************************/

      rqptr->rqCgi.HeaderLineCount += LineCount;
      rqptr->rqCgi.HeaderLength = HeaderLength = sptr - rqptr->rqCgi.HeaderPtr;

      /* eliminate header line(s) */
      memmove (OutputPtr, cptr, OutputCount - (cptr - OutputPtr));

      /* adjust for what we have buffered/eliminated of the header */
      OutputCount -= cptr - OutputPtr;

      if (!(rqptr->rqCgi.ProcessingBody = EndOfHeader)) return (OutputCount);

      /***************************/
      /* end of response header! */
      /***************************/

      if (rqptr->rqCgi.Header100Continue)
      {
         /****************/
         /* 100 continue */
         /****************/

         /* output a "100 Continue" header to the client */
         ResponseHeader100Continue (rqptr);

         /* reset the CGI header storage */
         CgiOutput (rqptr, NULL, 0);

         /* note that we've already done this once */
         rqptr->rqCgi.Header100ContinueDone = true;

         /* restart the header processing with anything that's left */
         if (OutputCount)
            OutputCount = CgiOutput (rqptr, OutputPtr, OutputCount);

         return (OutputCount);
      }

      /*****************/
      /* "real" header */
      /*****************/

      /* look at each line in the script-generated CGI response header */
      CgiCompliant = false;
      LineCount = 0;
      HeaderRemaining = rqptr->rqCgi.HeaderLength;
      cptr = rqptr->rqCgi.HeaderPtr;
      while (*cptr)
      { 
         /**************************/
         /* process header line(s) */
         /**************************/

         for (zptr = lptr = cptr;
              *zptr && *zptr != '\r' && !SAME2(zptr,'\r\n');
              zptr++);
         LineCount++;

         if (WATCHING (rqptr, WATCH_CGI))
            WatchThis (WATCHITM(rqptr), WATCH_CGI,
                       "RESPONSE header line !UL \'!#AZ\' !UL bytes",
                       LineCount, zptr-lptr, lptr, zptr-lptr);

         /* continue on to the beginning of (any) next line */
         if (*zptr == '\r') zptr++;
         if (*zptr == '\n') zptr++;
         HeaderRemaining -= zptr - lptr;

         if (LineCount == 1 &&
             MATCH7 (cptr, "HTTP/1."))
         {
            /*********************/
            /* absorb NPH header */
            /*********************/

            /* well, sort of! */
            CgiCompliant = true;
            /* bump to the start of the next line */
            if (!*(cptr = zptr)) break;
            continue;
         }

         /* ensure it looks like a response header field format */
         for (sptr = cptr;
              sptr < zptr && *sptr != ':' && !ISLWS(*sptr);
              sptr++);

         /* a little more restrictive than RFC2616 but... */
         if (!isalnum(*cptr) || *sptr != ':')
         {
            ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
            return (CGI_OUTPUT_NOT_STRICT);
         }

         if (to_upper(*cptr) == 'C' && to_upper(cptr[8]) == 'T' &&
             strsame (cptr, "Content-Type:", 13))
         {
            /****************/
            /* content-type */
            /****************/

            CgiCompliant = true;
            cptr += 13;
            while (ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
            sptr = cptr;
            while (NOTEOL(*cptr)) cptr++;
            if (!rqptr->rqCgi.AbsorbHeader)
            {
               rqptr->rqResponse.ContentTypePtr =
                  VmGetHeap (rqptr, cptr-sptr+1);
               memcpy (rqptr->rqResponse.ContentTypePtr, sptr, cptr-sptr);
               rqptr->rqResponse.ContentTypePtr[cptr-sptr] = '\0';
               rqptr->rqHeader.ContentTypePtr =
                  rqptr->rqResponse.ContentTypePtr;
            }

            if (strsame (sptr, "text/", 5))
               rqptr->rqCgi.ContentTypeText = true;
            else
               rqptr->rqCgi.ContentTypeText = false;

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         else
         if (to_upper(*cptr) == 'C' && to_upper(cptr[8]) == 'L' &&
             strsame (cptr, "Content-Length:", 15))
         {
            /******************/
            /* content-length */
            /******************/

            cptr += 15;
            while (ISLWS(*cptr)) cptr++;
            if (!rqptr->rqCgi.AbsorbHeader)
            {
               ulong  result;
               /* must be 0 to 4,294,967,295 anything else is out-of-range */
               result = strtoul (cptr, NULL, 10);
               if (!isdigit(*cptr) || result == ULONG_MAX)
               {
                  for (sptr = cptr; *sptr && isdigit(*sptr); sptr++);
                  if (WATCHING (rqptr, WATCH_CGI))
                     WatchThis (WATCHITM(rqptr), WATCH_CGI, "!#AZ !AZ",
                                sptr-cptr, cptr, strerror(errno));
                  return (CGI_OUTPUT_NOT_STRICT);
               }
               rqptr->rqCgi.ContentLength = result;
               if (WATCHING (rqptr, WATCH_CGI))
                  WatchThis (WATCHITM(rqptr), WATCH_CGI, "!&,UL bytes", result);

               /* eliminate this header line */
               rqptr->rqCgi.HeaderLength -= zptr - lptr;
               memmove (lptr, zptr, HeaderRemaining+1);
               zptr -= zptr - lptr;
            }
         }
         else
         if (to_upper(*cptr) == 'C'  && to_upper(cptr[8]) == 'E' &&
             strsame (cptr, "Content-Encoding:", 17))
         {
            /********************/
            /* content-encoding */
            /********************/

            cptr += 17;
            while (ISLWS(*cptr)) cptr++;
            if (strsame (cptr, "gzip", 4))
               rqptr->rqResponse.ContentIsEncodedGzip = true;
            else
               rqptr->rqResponse.ContentIsEncodedUnknown = true;
         }
         else
         if (to_upper(*cptr) == 'C' && to_upper(cptr[8]) == 'S' &&
             strsame (cptr, "Content-Security-Policy:", 24))
         {
            /***************************/
            /* content-security-policy */
            /***************************/

            cptr += 24;
            while (ISLWS(*cptr)) cptr++;
            for (sptr = cptr; *cptr && *cptr != '\r' && *cptr != '\n'; cptr++);
            rqptr->rqCgi.CspLength = cptr - sptr;
            rqptr->rqCgi.CspPtr = VmGetHeap (rqptr, rqptr->rqCgi.CspLength+1);
            memcpy (rqptr->rqCgi.CspPtr, sptr, rqptr->rqCgi.CspLength);

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         else
         if (to_upper(*cptr) == 'S' && strsame (cptr, "Status:", 7))
         {
            /**********/
            /* status */
            /**********/

            CgiCompliant = true;
            cptr += 7;
            /* create HTTP header status line using supplied status */
            while (*cptr++ && !isdigit(*cptr) && *cptr != '\n') cptr++;
            if (!rqptr->rqCgi.AbsorbHeader)
            {
               /* get response status code for header and logging purposes */
               if (isdigit(*cptr)) rqptr->rqResponse.HttpStatus = atoi(cptr);

               if (rqptr->rqResponse.HttpStatus == 101 &&
                   rqptr->WebSocketRequest)
               {
                  /******************************************/
                  /* 101 switching protocols (to WebSocket) */
                  /******************************************/

                  rqptr->rqCgi.Header101Switching =
                      rqptr->rqCgi.AbsorbHeader = true;
               }
            }

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         else
         if (to_upper(*cptr) == 'S' && strsame (cptr, "Script-Control:", 15))
         {
            /*****************************************/
            /* script control (Apache Group CGI/1.2) */
            /*****************************************/

            cptr += 15;
            while (*cptr && NOTEOL(*cptr))
            {
               while (ISLWS(*cptr) || *cptr == ',' || *cptr == ';') cptr++;
               if (ISEOL(*cptr)) break;
               if (cnt = CgiScriptControlField (rqptr, cptr))
                  cptr += cnt;
               else
               {
                  /* discard unknown script-control */
                  char  Scratch [1024];
                  StringParseValue (&cptr, Scratch, sizeof(Scratch));
               }
            }

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         else
         if (to_upper(*cptr) == 'T' && strsame (cptr, "Transfer-Encoding:", 18))
         {
            /*********************/
            /* transfer-encoding */
            /*********************/

            /* take note the response body is transfer-encoded in some way */
            rqptr->rqCgi.TransferEncoding = true;
         }
         else
         if (to_upper(*cptr) == 'X' && strsame (cptr, "X-vms-record-mode:", 18))
         {
            /*****************************/
            /* VMS Apache record control */
            /*****************************/

            /*
               VMS Apache (in pre-CSWS days) uses an eXtension header field
               so that the script can convey to the server whether the output
               should be processed as a binary stream or as records.
               (Keep this in post-CSWS days for backward compatibility.)
            */

            cptr += 18;
            while (ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
            /* this value should either 0 or 1 ("no" or "yes") plus 1 */
            rqptr->rqCgi.XVMSRecordMode = atoi(cptr)+1;

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         else
         if (to_upper(*cptr) == 'L' && strsame (cptr, "Location:", 9))
         {
            /************/
            /* location */
            /************/

            CgiCompliant = true;
            cptr += 9;
            while (ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
            sptr = cptr;
            while (NOTEOL(*cptr)) cptr++;
            if (!rqptr->rqCgi.AbsorbHeader)
               ResponseLocation (rqptr, sptr, cptr-sptr);

            /* eliminate this header line */
            rqptr->rqCgi.HeaderLength -= zptr - lptr;
            memmove (lptr, zptr, HeaderRemaining+1);
            zptr -= zptr - lptr;
         }
         
         /* bump to the start of the next (if any) line */
         if (!*(cptr = zptr)) break;
      }

      if (cptr = rqptr->rqPathSet.ScriptControlPtr)
      {
         if (WATCHING (rqptr, WATCH_CGI))
            WatchThis (WATCHITM(rqptr), WATCH_CGI, "!AZ", cptr);
         while (*cptr)
         {
            while (ISLWS(*cptr) || *cptr == ',' || *cptr == ';') cptr++;
            if (!*cptr) break;
            if (cnt = CgiScriptControlField (rqptr, cptr))
               cptr += cnt;
            else
            {
               /* discard unknown script-control */
               char  Scratch [1024];
               StringParseValue (&cptr, Scratch, sizeof(Scratch));
            }
         }
      }

      if (WATCHING (rqptr, WATCH_CGI))
         if (rqptr->rqCgi.AbsorbHeader)
            WatchThis (WATCHITM(rqptr), WATCH_CGI, "RESPONSE header absorbed");

      /****************************/
      /* generate response header */
      /****************************/

      if (!rqptr->rqCgi.OutputMode)
      {
         if (rqptr->rqCgi.XVMSRecordMode)
         {
            if (rqptr->rqCgi.XVMSRecordMode-1)
               rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
            else
               rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
         }
         else
         /* always enforce stream mode if it's encoded in some way! */
         if (rqptr->rqResponse.ContentIsEncodedGzip ||
             rqptr->rqResponse.ContentIsEncodedUnknown)
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
         else
         if (rqptr->rqCgi.ContentTypeText ||
            rqptr->rqCgi.Header101Switching)
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
         else
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
      }

      if (WATCHING (rqptr, WATCH_CGI))
      {
         switch (rqptr->rqCgi.OutputMode)
         {
            case CGI_OUTPUT_MODE_CRLF : cptr = "crlf"; break;
            case CGI_OUTPUT_MODE_RECORD0 : cptr = "record0"; break;
            case CGI_OUTPUT_MODE_RECORD : cptr = "record"; break;
            case CGI_OUTPUT_MODE_STREAM : cptr = "stream"; break;
            default : cptr = "?";
         }
         WatchThis (WATCHITM(rqptr), WATCH_CGI, "RESPONSE !AZ mode", cptr);
      }

      if (rqptr->rqCgi.Header101Switching)
      {
         /***************************/
         /* 101 switching protocols */
         /***************************/

         /* output the Web Socket handshake header to the client */
         WebSockSwitchResponse (rqptr);

         /* delay WebSocket I/O ASTs until after response header */
         WebSockInput (rqptr);
         WebSockOutput (rqptr);

         return (OutputCount);
      }

      if (!CgiCompliant)
      {
         ErrorNoticed (rqptr, 0, CgiNotStrict, FI_LI);
         return (CGI_OUTPUT_NOT_STRICT);
      }

      if (rqptr->rqCgi.ScriptControlErrorTextPtr)
      {
         /* script is requesting the server generate an error message */
         sptr = rqptr->rqCgi.ScriptControlErrorModulePtr;
         cnt = rqptr->rqCgi.ScriptControlErrorLine;
         if (status = rqptr->rqCgi.ScriptControlErrorVmsStatus)
         {
            rqptr->rqResponse.ErrorTextPtr =
               rqptr->rqCgi.ScriptControlErrorTextPtr;
            rqptr->rqResponse.ErrorOtherTextPtr =
               rqptr->rqCgi.ScriptControlErrorVmsTextPtr;
            if (sptr)
               ErrorVmsStatus (rqptr, status, sptr, cnt);
            else
               ErrorVmsStatus (rqptr, status, FI_LI);
         }   
         else
         {
            cptr = rqptr->rqCgi.ScriptControlErrorTextPtr; 
            if (sptr)
               ErrorGeneral (rqptr, cptr, sptr, cnt);
            else
               ErrorGeneral (rqptr, cptr, FI_LI);
         }
      }
      else
      if (!rqptr->rqCgi.AbsorbHeader)
      {
         if (rqptr->rqPathSet.CacheCGI &&
             !rqptr->rqCache.LoadCheck)
         {
            /* script output to be cached */
            rqptr->rqCache.LoadFromCgi =
               CacheLoadBegin (rqptr, (int)rqptr->rqResponse.ContentLength64,
                                      rqptr->rqResponse.ContentTypePtr);
            if (rqptr->rqCache.LoadFromCgi &&
                rqptr->rqCgi.HeaderLength)
            {
               /* plus one to include the terminating null */
               CacheLoadData (rqptr, rqptr->rqCgi.HeaderPtr,
                                     rqptr->rqCgi.HeaderLength+1);
            }
         } 

         /* if it was a 'redirect' and no other response status was supplied */
         if (rqptr->rqResponse.LocationPtr &&
             !rqptr->rqResponse.HttpStatus)
             rqptr->rqResponse.HttpStatus = 302;

         ResponseHeader (rqptr, rqptr->rqResponse.HttpStatus,
                         rqptr->rqResponse.ContentTypePtr,
                         rqptr->rqCgi.ContentLength,
                         NULL,
                         rqptr->rqCgi.HeaderPtr);
      }

      if (rqptr->WebSocketRequest)
      {
         /*************************/
         /* shouldn't get to here */
         /*************************/

         /* encourage writing of the response header */
         NetWrite (rqptr, &RequestEnd, NULL, 0);

         return (CGI_OUTPUT_END);
      }

      if (!OutputCount) return (OutputCount);
   }

   /*************************/
   /* HTTP carriage-control */
   /*************************/

   if (rqptr->rqCgi.FilterStream)
   {
      /* remove all non-printable characters */
      for (zptr=(cptr=OutputPtr)+OutputCount;
           (isprint(*cptr) || isspace(*cptr)) && cptr < zptr;
           cptr++);
      if (cptr < zptr)
      {
         /* a non-printable was detected, now eliminate it and any following */
         sptr = cptr;
         while (cptr < zptr)
         {
            while (!(isprint(*cptr) || isspace(*cptr)) && cptr < zptr) cptr++;
            while ((isprint(*cptr) || isspace(*cptr)) && cptr < zptr)
               *sptr++ = *cptr++;
         }
         *sptr = '\0';
         OutputCount = sptr - OutputPtr;
      }
   }

   if (rqptr->rqCgi.OutputMode == CGI_OUTPUT_MODE_RECORD ||
       rqptr->rqCgi.IsCliDcl)
   {
      if (OutputCount)
      {
         if (OutputPtr[OutputCount-1] != '\n')
         {
            OutputPtr[OutputCount++] = '\n';
            OutputPtr[OutputCount] = '\0';
         }
      }
      else
      {
         OutputPtr[OutputCount++] = '\n';
         OutputPtr[OutputCount] = '\0';
      }

      if (rqptr->FileContentPtr)
      {
         CgiOutputFile (rqptr, OutputPtr, OutputCount);
         return (0);
      }
      if (rqptr->rqCache.LoadFromCgi)
         CacheLoadData (rqptr, OutputPtr, OutputCount);

      return (OutputCount);
   }

   if (rqptr->rqCgi.OutputMode == CGI_OUTPUT_MODE_RECORD0)
   {
      /* only have empty records as newlines */
      if (!OutputCount)
      {
         OutputPtr[OutputCount++] = '\n';
         OutputPtr[OutputCount] = '\0';
      }

      if (rqptr->FileContentPtr)
      {
         CgiOutputFile (rqptr, OutputPtr, OutputCount);
         return (0);
      }
      if (rqptr->rqCache.LoadFromCgi)
         CacheLoadData (rqptr, OutputPtr, OutputCount);

      return (OutputCount);
   }

   if (rqptr->rqCgi.OutputMode == CGI_OUTPUT_MODE_CRLF)
   {
      if (!OutputCount ||
          OutputPtr[OutputCount-1] != '\n' ||
          (OutputCount >= 2 && OutputPtr[OutputCount-2] != '\r'))
      {
         /* record0 record, or no trailing <LF>, or no trailing <CR><LF> */
         if (OutputCount && OutputPtr[OutputCount-1] == '\n') OutputCount--;
         OutputPtr[OutputCount++] = '\r';
         OutputPtr[OutputCount++] = '\n';
         OutputPtr[OutputCount] = '\0';
         if (rqptr->FileContentPtr)
         {
            CgiOutputFile (rqptr, OutputPtr, OutputCount);
            return (0);
         }
         if (rqptr->rqCache.LoadFromCgi)
            CacheLoadData (rqptr, OutputPtr, OutputCount);
         return (OutputCount);
      }
   }
   else
   if (!OutputCount)
   {
      /* stream-mode with zero bytes - do nothing, just return */
      return (OutputCount);
   }

   if (rqptr->FileContentPtr)
   {
      CgiOutputFile (rqptr, OutputPtr, OutputCount);
      return (0);
   }
   if (rqptr->rqCache.LoadFromCgi)
      CacheLoadData (rqptr, OutputPtr, OutputCount);

   return (OutputCount);
}

/*****************************************************************************/
/*
Process a CGI/1.2 "Script-Control:" response header field.  Most of these are
WASD extensions.  Returns the number of characters parsed from the line.
*/ 

int CgiScriptControlField
(
REQUEST_STRUCT *rqptr,
char *ControlString
)
{                                           
   int  size, Number;
   char  *cptr, *sptr, *zptr;
   char  Scratch [1024];

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

   if (WATCH_MODULE(WATCH_MOD_CGI))
   {
      for (cptr = ControlString; *cptr && NOTEOL(*cptr); cptr++);
      if (ISEOL(*cptr)) cptr--;
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                 "CgiScriptControlField() !#AZ",
                 cptr-ControlString+1, ControlString);
   }

   cptr = ControlString;

   /* the one and only "official" field of CGI/1.2 :^) */
   if (strsame (cptr, "no-abort", 8))
   {
      HttpdTimerSet (rqptr, TIMER_OUTPUT, -1);
      HttpdTimerSet (rqptr, TIMER_NOPROGRESS, -1);
      cptr += 8;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }

   /**************************/
   /* carriage-control modes */
   /**************************/

   if (strsame (cptr, "X-crlf-mode", 11))
   {
      cptr += 11;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
         else
         if (*cptr == '1')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_CRLF;
      }
      else
         rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_CRLF;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-record-mode", 13))
   {
      cptr += 13;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
         else
         if (*cptr == '1')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
      }
      else
         rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-record0-mode", 14))
   {
      cptr += 14;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
         else
         if (*cptr == '1')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD0;
      }
      else
         rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD0;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-stream-mode", 13))
   {
      cptr += 13;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_RECORD;
         else
         if (*cptr == '1')
            rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
      }
      else
         rqptr->rqCgi.OutputMode = CGI_OUTPUT_MODE_STREAM;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }

   /*******************/
   /* stream handling */
   /*******************/

   if (strsame (cptr, "X-buffer-records", 16))
   {
      cptr += 16;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.BufferRecords = false;
         else
         if (*cptr == '1')
            rqptr->rqCgi.BufferRecords = true;
      }
      else
         rqptr->rqCgi.BufferRecords = true;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-content-encoding-gzip", 23))
   {
      cptr += 23;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.ContentEncodingGzip = CGI_OUTPUT_NO_GZIP;
         else
         if (*cptr == '1')
            rqptr->rqCgi.ContentEncodingGzip = CGI_OUTPUT_GZIP;
      }
      else
         rqptr->rqCgi.ContentEncodingGzip = CGI_OUTPUT_GZIP;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-filter-stream", 15))
   {
      cptr += 15;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.FilterStream = false;
         else
         if (*cptr == '1')
            rqptr->rqCgi.FilterStream = true;
      }
      else
         rqptr->rqCgi.FilterStream = true;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }
   if (strsame (cptr, "X-transfer-encoding-chunked", 27))
   {
      cptr += 27;
      if (*cptr == '=')
      {
         cptr++;
         if (*cptr == '0')
            rqptr->rqCgi.TransferEncodingChunked = CGI_OUTPUT_NO_CHUNK;
         else
         if (*cptr == '1')
            rqptr->rqCgi.TransferEncodingChunked = CGI_OUTPUT_CHUNK;
      }
      else
         rqptr->rqCgi.ContentEncodingGzip = CGI_OUTPUT_CHUNK;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }

   /************************/
   /* server cache control */
   /************************/

   if (strsame (cptr, "X-cache-", 8))
   {
      if (strsame (cptr+8, "max=", 4))
      {
         cptr += 12;
         if (strsame (cptr, "none", 4))
            Number = 0;
         else
            Number = atoi(cptr);
         rqptr->rqCgi.ScriptControlCacheMaxKBytes = Number;
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
      if (strsame (cptr+8, "expires=", 8))
      {
         cptr += 16;
         if (strsame (cptr, "day", 3))
            Number = CACHE_EXPIRES_DAY;
         else
         if (strsame (cptr, "hour", 4))
            Number = CACHE_EXPIRES_HOUR;
         else
         if (strsame (cptr, "minute", 6))
            Number = CACHE_EXPIRES_MINUTE;
         else
         if (*cptr == '0')
            Number = CACHE_EXPIRES_NONE;
         else
         {
            /* if it's not a 'hh:mm:ss' format then it's in minutes */
            Number = MetaConSetSeconds (NULL, cptr, 60);
            if (Number < 0) Number = 0;
         }
         rqptr->rqCgi.ScriptControlCacheExpiresAfter = Number;
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
   }

   /**********/
   /* timers */
   /**********/

   if (strsame (cptr, "X-timeout-", 10))
   {
      if (strsame (cptr+10, "output=", 7))
      {
         cptr += 17;
         if (strsame (cptr, "none", 4))
            Number = -1;
         else
            Number = atoi(cptr);
         HttpdTimerSet (rqptr, TIMER_OUTPUT, Number);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
      if (strsame (cptr+10, "noprogress=", 11))
      {
         cptr += 21;
         if (strsame (cptr, "none", 4))
            Number = -1;
         else
            Number = atoi(cptr);
         HttpdTimerSet (rqptr, TIMER_NOPROGRESS, Number);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
   }
   if (rqptr->DclTaskPtr &&
       strsame (cptr, "X-lifetime=", 11))
   {
      cptr += 11;
      if (strsame (cptr, "none", 4))
         Number = -1;
      else
      {
         if (!(Number = atoi(cptr) * 60))
         {
            if (rqptr->DclTaskPtr->TaskType == DCL_TASK_TYPE_CGI_SCRIPT)
               Number = Config.cfScript.ZombieLifeTime+1;
            else
               Number = Config.cfScript.CgiPlusLifeTime+1;
         }
      }
      rqptr->DclTaskPtr->LifeTimeSecond = Number;
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }

   /********************************/
   /* error message control fields */
   /********************************/

   if (strsame (cptr, "X-error-", 8))
   {
      if (strsame (cptr+8, "line=", 5))
      {
         cptr += 13;
         rqptr->rqCgi.ScriptControlErrorLine = atoi(cptr);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
      if (strsame (cptr+8, "module=", 7))
      {
         cptr += 15;
         zptr = cptr;
         StringParseValue (&cptr, Scratch, sizeof(Scratch));
         sptr = VmGetHeap (rqptr, size = cptr-zptr+1);
         strzcpy (sptr, Scratch, size);
         rqptr->rqCgi.ScriptControlErrorModulePtr = sptr;
         return (cptr - ControlString);
      }
      if (strsame (cptr+8, "text=", 5))
      {
         cptr += 13;
         zptr = cptr;
         StringParseValue (&cptr, Scratch, sizeof(Scratch));
         sptr = VmGetHeap (rqptr, size = cptr-zptr+1);
         strzcpy (sptr, Scratch, size);
         rqptr->rqCgi.ScriptControlErrorTextPtr = sptr;
         return (cptr - ControlString);
      }
      if (strsame (cptr+8, "vms-status=", 11))
      {
         cptr += 19;
         if (SAME2(cptr,'%x') || SAME2(cptr,'%X'))
            rqptr->rqCgi.ScriptControlErrorVmsStatus = strtol(cptr+2, NULL, 16);
         else
            rqptr->rqCgi.ScriptControlErrorVmsStatus = atoi(cptr);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
         return (cptr - ControlString);
      }
      if (strsame (cptr+8, "vms-text=", 9))
      {
         cptr += 17;
         zptr = cptr;
         StringParseValue (&cptr, Scratch, sizeof(Scratch));
         sptr = VmGetHeap (rqptr, size = cptr-zptr+1);
         strzcpy (sptr, Scratch, size);
         rqptr->rqCgi.ScriptControlErrorVmsTextPtr = sptr;
         return (cptr - ControlString);
      }
   }

   /*******************/
   /* content handler */
   /*******************/

   if (strsame (cptr, "X-content-handler=", 18))
   {
      cptr += 18;
      StringParseValue (&cptr, Scratch, sizeof(Scratch));
      if (strsame (Scratch, "SSI", -1))
      {
         if (!rqptr->SsiTaskPtr &&
             !rqptr->FileContentPtr)
         {
            /* initialize the file contents structure */
            CgiOutputFile (rqptr, NULL, SsiSizeMax);
            /* set the content structure handler so SSI engine gets control */
            rqptr->FileContentPtr->ContentHandlerFunction = &SsiBegin;
            /* don't want the CGI header in the contents */
            rqptr->rqCgi.AbsorbHeader = true;
         }
      }
      return (cptr - ControlString);
   }

   /**************************/
   /* force this HTTP status */
   /**************************/

   if (strsame (cptr, "X-http-status=", 14))
   {
      /* applicable only to an error reporting script */
      cptr += 14;
      rqptr->rqCgi.ScriptControlHttpStatus = atoi(cptr);
      while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && *cptr != ';') cptr++;
      return (cptr - ControlString);
   }

   /************************/
   /* ignore anything else */
   /************************/

   if (WATCHING (rqptr, WATCH_CGI))
   {
      WatchThis (WATCHITM(rqptr), WATCH_CGI,
                 "UNRECOGNISED script control field");
      WatchDataFormatted ("!&Z", cptr);
   }
   return (0);
}

/*****************************************************************************/
/*
Capturing script output to a 'file contents' structure.  This will be detected
and 'handled' by RequestEnd().
*/ 

void CgiOutputFile
(
REQUEST_STRUCT *rqptr,
char *OutputPtr,
int OutputCount
)
{                                           
#define CONTENT_SIZE_INITIAL 8192

   int  ContentSize;
   char  *cptr, *sptr, *zptr;
   FILE_CONTENT  *fcptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
   {
      if (fcptr = rqptr->FileContentPtr)
         WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                    "CgiOutputFile() !UL !UL !UL !UL !&X",
                    fcptr->ContentSizeMax, fcptr->ContentSize, 
                    fcptr->ContentLength, OutputCount, OutputPtr);
      else
         WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI,
                    "CgiOutputFile() !UL !&X", OutputCount, OutputPtr);
      if (OutputPtr) WatchDataDump (OutputPtr, OutputCount);
   } 

   if (!OutputPtr)
   {
      /**************/
      /* initialize */
      /**************/

      ContentSize = CONTENT_SIZE_INITIAL;
      if (ContentSize > OutputCount) ContentSize = OutputCount;

      /* allow one extra character for a terminating null */
      rqptr->FileContentPtr = fcptr = (FILE_CONTENT*)
         VmGetHeap (rqptr, sizeof(FILE_CONTENT) + ContentSize+1);

      fcptr->ContentSizeMax = OutputCount;
      fcptr->ContentSize = ContentSize;

      if (WATCHING (rqptr, WATCH_CGI))
         WatchThis (WATCHITM(rqptr), WATCH_CGI,
                    "CONTENT !UL bytes max", fcptr->ContentSizeMax);

      /* buffer space immediately follows the structured storage */
      fcptr->ContentPtr = (char*)fcptr + sizeof(FILE_CONTENT);

      /* populate the file contents structure with some file data */
      zptr = (sptr = fcptr->FileName) + sizeof(fcptr->FileName);
      for (cptr = rqptr->rqCgi.ScriptFileNamePtr;
           *cptr && sptr < zptr;
           *sptr++ = *cptr++);
      if (sptr >= zptr)
      {
         ErrorGeneralOverflow (rqptr, FI_LI);
         return;
      }
      *sptr = '\0';
      fcptr->FileNameLength = sptr - fcptr->FileName;

      fcptr->CdtTime64 = rqptr->rqTime.BeginTime64;
      fcptr->RdtTime64 = rqptr->rqTime.BeginTime64;

      fcptr->UicGroup = 0;
      fcptr->UicMember = 0;
      fcptr->Protection = 0;

      return;
   }

   /**********/
   /* buffer */
   /**********/

   fcptr = rqptr->FileContentPtr;

   if (fcptr->ContentLength + OutputCount > fcptr->ContentSize)
   {
      if (fcptr->ContentSize >= fcptr->ContentSizeMax)
      {
         /* keep track of how large it would have grown */
         fcptr->ContentLength += OutputCount;
         return;
      }

      /* double it's size each time, up to the size limit */
      fcptr->ContentSize += fcptr->ContentSize;
      if (fcptr->ContentSize > fcptr->ContentSizeMax)
         fcptr->ContentSize = fcptr->ContentSizeMax;

      rqptr->FileContentPtr = fcptr = (FILE_CONTENT*)
         VmReallocHeap (rqptr, rqptr->FileContentPtr,
                        sizeof(FILE_CONTENT) + fcptr->ContentSize+1, FI_LI);
      fcptr->ContentPtr = (char*)fcptr + sizeof(FILE_CONTENT);
   }

   memcpy (fcptr->ContentPtr + fcptr->ContentLength, OutputPtr, OutputCount); 
   fcptr->ContentLength += OutputCount;
   fcptr->ContentPtr[fcptr->ContentLength] = '\0';

   if (WATCHMOD (rqptr, WATCH_MOD_CGI))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_CGI, "!UL !UL !UL",
                 fcptr->ContentSizeMax, fcptr->ContentSize, 
                 fcptr->ContentLength);
}

/*****************************************************************************/
/*
Generate a (hopefully) unique sequence of 224 bits (28 bytes, which is a fair
bit :^)  The string is generated using three pseudo-random numbers (seeded each
second from binary time) each time it is called.  This provides a continuous,
non-repeating series of unlikely bit combinations with a one in 2^224 chance (I
think!) of presence in an I/O stream.
*/ 

void CgiSequence
(
char OneChar,
char *CgiSequencePtr,
int *CgiSequenceLengthPtr
)
{                                           
   static $DESCRIPTOR (SequenceFaoDsc, "$!AZ-!8XL!8XL!8XL-\0");
   static $DESCRIPTOR (SequenceDsc, "");
   static char  OneCharString [] = " ";
   static int64  PrevTime64,
                 RandomNumber;

   unsigned short  slen;
   unsigned long  rand1, rand2, rand3;

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

   if (WATCH_MODULE(WATCH_MOD_CGI))
      WatchThis (WATCHALL, WATCH_MOD_CGI, "CgiSequence()");

   /* (re)seed the random number every second or so */
   if (PrevTime64 != HttpdTime64)
      RandomNumber += (PrevTime64 = HttpdTime64);

   /* cheap (no subroutine call) MTH$RANDOM() */
   rand1 = RandomNumber = RandomNumber * 69069 + 1;
   rand2 = RandomNumber = RandomNumber * 69069 + 1;
   rand3 = RandomNumber = RandomNumber * 69069 + 1;
   SequenceDsc.dsc$w_length = 29;
   SequenceDsc.dsc$a_pointer = CgiSequencePtr;
   OneCharString[0] = OneChar;
   sys$fao (&SequenceFaoDsc, &slen, &SequenceDsc,
            OneCharString, rand1, rand2, rand3);
   *CgiSequenceLengthPtr = slen-1;

   if (WATCH_MODULE(WATCH_MOD_CGI))
      WatchThis (WATCHALL, WATCH_MOD_CGI, "!&Z", CgiSequencePtr);
}

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

