/*****************************************************************************/
/*
                                 HTTP2net.c

HTTP/2 network reads and writes.  Reads are multiplexed onto the connection by
the client and similarly multiplexed off at the server end.  The frames read
are then handed off to the appropriate processing function by frame type. 
Writes are serialised onto the connection using a FIFO queue.  Well, four
(prioritised) queues to be precise.  Request (network) reads and writes have
wrapper functions that interface the single HTTP/2 multiplexed connection to an
emulation of the dedicated, per-request network connection.

All request data being written to the network is required to be prepended with
a 9 byte frame header.  Without copying a lot of data between buffers this is
implemented using two, independent writes; the header then the data.  In an
effort to reduce the overall number of such network writes (especially when
transported by SSL), *all* WASD memory allocations have inbuilt space for an
HTTP/2 frame header immediately preceding the returned memory pointer.  This
space has some (not very distant) magic (VM_MAGIC_HTTP2) to indicate it is
present.  So, when this is detected the HTTP/2 frame header fields can be
written to the space immediately preceding the data buffer, saving on the
separate, small (and relatively costly) frame header write, reducing *all*
request data written from two to a single write without adding significant
complication to memory or data buffer management.  Hopefully not too clever for
its own good!  Of course it is also possible that a write originates from
non-allocated memory (e.g. string constant) and does not contain this header
and so requires a separate header write.


VERSION HISTORY
---------------
25-AUG-2024  MGD  bugfix; request I/O accounting with HTTP/2
04-MAR-2024  MGD  refine HTTP/2 flow control
27-OCT-2023  MGD  refinements using https://github.com/summerwind/h2spec
27-JUL-2022  MGD  bugfix; when using file cache magic buffers
31-JAN-2020  MGD  fiddle with the code (perhaps optimisations, even bugfixes)
15-FEB-2019  MGD  bugfix; Http2NetQueueWrite() PEEK_8 at w2ptr->type
01-JUL-2018  MGD  bugfix; Http2NetIoWrite() blocking write data must be
                    asynchronously persistent so employ internal buffer(s)
06-JAN-2018  MGD  refactor write code paths (simplify and greater efficiency)
11-FEB-2017  MGD  bugfix; Http2NetQueueWrite() and Http2NetWriteDataAst() 
                    blocking writes are not placed on the request's
                    write list as they are transparent to the request
                  bugfix; Http2NetQueueWrite() deliver via NetIoWriteStatus()
                    using SS$_NORMAL (HTTP/2 I/O) not the request ->VmsStatus
06-AUG-2016  MGD  last stream ident now noted by Http2RequestBegin()
16-AUG-2015  MGD  initial
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#  undef __VMS_VER
#  define __VMS_VER 70000000
#  undef __CRTL_VER
#  define __CRTL_VER 70000000
#else
#  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
#endif

#include <stdio.h>
#include <ctype.h>

#include "wasd.h"

#define WASD_MODULE "HTTP2NET"

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

extern uint  Http2ClientPrefaceLength,
             Http2MaxFrameSize;

extern ulong  HttpdTickSecond;

extern char  ErrorSanityCheck[],
             Http2ClientPreface[];

extern LIST_HEAD  Http2List;

extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initial read of the data described below.
*/

int Http2NetClientRead (HTTP2_STRUCT *h2ptr)

{
   int  status;

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

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
                 "Http2NetClientRead() !&X", h2ptr->NetIoPtr);

   h2ptr->ReadBufferCount = 0;

   /* stop reading when the HTTP/2 connection is closing */
   if (h2ptr->ConnectClosing) return (SS$_NORMAL);

   h2ptr->ReadInUseCount++;

   status = NetIoRead (h2ptr->NetIoPtr, Http2NetClientReadAst, h2ptr,
                       h2ptr->ReadBufferPtr, h2ptr->ReadBufferSize);

   return (status);
}

/*****************************************************************************/
/*
AST of read from client with sufficient data to parse (at least) the frame
header (9 octets) so the length of the frame can be determined.  Multiple reads
may be performed to build up a complete frame in the read buffer (potentially
more than one frame).  Only a complete frame is parsed and processed.  This
function always expects the frame to be located at the beinning of the read
buffer.
*/

void Http2NetClientReadAst (HTTP2_STRUCT *h2ptr)

{
   int  count, length, retval, status;
   uint  flags, ident, type;
   uchar  *bptr;
   HTTP2_STREAM_STRUCT  *s2ptr;

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

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
   {
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
"Http2NetClientReadAst() !&F !UL !&X !&S !UL", Http2NetClientReadAst,
                 h2ptr->ReadInUseCount, h2ptr->NetIoPtr,
                 h2ptr->NetIoPtr->ReadStatus, h2ptr->NetIoPtr->ReadCount);
      WatchDataDump (h2ptr->ReadBufferPtr, h2ptr->NetIoPtr->ReadCount);
   }

   if (h2ptr->ReadInUseCount != 1)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   if (h2ptr->ReadInUseCount) h2ptr->ReadInUseCount--;

   if (h2ptr->ConnectClosing)
   {
      Http2ConnectClose (h2ptr);
      return;
   }

   /* if the connection has a set VMS status */
   if (h2ptr->NetIoPtr->VmsStatus)
      h2ptr->NetIoPtr->ReadStatus = h2ptr->NetIoPtr->WriteStatus =
            h2ptr->NetIoPtr->VmsStatus;

   /* if the connection presented a read error */
   if (VMSnok (h2ptr->NetIoPtr->ReadStatus))
   {
      Http2ConnectClose (h2ptr);
      return;
   }

   h2ptr->BytesRawRx64 += h2ptr->NetIoPtr->ReadCount;
   h2ptr->BytesRawTallyRx64 += h2ptr->NetIoPtr->ReadCount;
   h2ptr->ReadBufferCount += h2ptr->NetIoPtr->ReadCount;

   bptr = h2ptr->ReadBufferPtr;
   count = h2ptr->ReadBufferCount;

   while (count >= HTTP2_FRAME_HEADER_SIZE)
   {
      if (h2ptr->ExpectingH2cPreface)
      {
         /* RFC 7540 3.2 Starting HTTP/2 for "http" URIs */
         if (count >= Http2ClientPrefaceLength &&
             MATCH8 (bptr, Http2ClientPreface) &&
             MATCH0 (bptr, Http2ClientPreface, Http2ClientPrefaceLength))
         {
            if (WATCHING (h2ptr, WATCH_HTTP2))
               WatchThis (WATCHITM(h2ptr), WATCH_HTTP2,
                          "HTTP/2 connection preface (h2c)");
            h2ptr->ExpectingH2cPreface = false;
            count -= Http2ClientPrefaceLength;
            bptr += Http2ClientPrefaceLength;
            continue;
         }
         Http2DoError (h2ptr, 0, HTTP2_ERROR_PROTOCOL, FI_LI);
         return;
      }

      HTTP2_PEEK_24 (bptr, length);

      if (WATCHPNT(h2ptr) && (WATCH_CATEGORY(WATCH_HTTP2) ||
                              WATCH_MODULE(WATCH_MOD_HTTP2)))
      {
         if (count >= length)
            Http2WatchFrame (h2ptr, bptr, NULL, count, false);
         else
         {
            HTTP2_PEEK_8 (bptr+3, type);
            HTTP2_PEEK_8 (bptr+4, flags);
            HTTP2_PEEK_32 (bptr+5, ident);
            WatchThis (WATCHITM(h2ptr), WATCH_HTTP2,
"FRAME !SL count:!UL length:!UL type:!UL flags:0x!2ZL ident:!UL",
                       count-length, count, length, type, flags, ident);
         }
      }

      if (length > h2ptr->ClientMaxFrameSize ||
          length > h2ptr->ServerMaxFrameSize)
      {
         retval = Http2DoError (h2ptr, 0, HTTP2_ERROR_SIZE, FI_LI);
         if (retval < 0)
         {
            Http2NetClientEnd (h2ptr, ident);
            return;
         }
      }

      /* if not a complete frame */
      if (length + HTTP2_FRAME_HEADER_SIZE > count) break;

      HTTP2_GET_24 (bptr, length);
      HTTP2_GET_8 (bptr, type);
      HTTP2_GET_8 (bptr, flags);
      HTTP2_GET_32 (bptr, ident);
      /* ignore any reserved bit */
      ident &= 0x7fffffff;

      count -= HTTP2_FRAME_HEADER_SIZE;

      h2ptr->FrameCountRx++;
      h2ptr->FrameTallyRx++;

      /* mitigate CVE-2019-9518 */
      if (!length && !(flags & HTTP2_FLAG_END_STR))
      {
         switch (type)
         {
            case HTTP2_FRAME_DATA :
            case HTTP2_FRAME_HEADERS :
            case HTTP2_FRAME_CONTINUATION :
            case HTTP2_FRAME_PUSH_PROMISE :
            if (h2ptr->EmptyFrameLimitCount++ > HTTP2_EMPTY_FRAME_LIMIT_COUNT)
            {
               if (WATCHING (h2ptr, WATCH_HTTP2))
                  WatchThis (WATCHITM(h2ptr), WATCH_HTTP2,
                             "LIMIT empty frames to !UL/S (DoS?)",
                             HTTP2_EMPTY_FRAME_LIMIT_COUNT);
               /* calm is a stream error not fatal correction */
               Http2DoError (h2ptr, 0, HTTP2_ERROR_CALM, FI_LI);
               count -= length;
               bptr += length;
               continue;
            }
         }
      }

      s2ptr = NULL;
      if (ident)
      {
         /*********/
         /* ident */
         /*********/

         h2ptr->FrameRequestCountRx++;
         h2ptr->FrameRequestTallyRx++;
         ident &= 0x7fffffff;
         if (!(ident & 1))
         {
            /* client initiated streams must have odd numbered idents */
            retval = Http2DoError (h2ptr, 0, HTTP2_ERROR_PROTOCOL, FI_LI);
            return;
         }

         if (h2ptr->HeaderThisIdent)
         {
            if (type != HTTP2_FRAME_CONTINUATION)
            {
               retval = Http2DoError (h2ptr, 0, HTTP2_ERROR_PROTOCOL, FI_LI);
               break;
            }
         }

         if (h2ptr->GoAwayIdent)
         {
            /* ignore frames with idents after server signals goaway */
            count -= length;
            bptr += length;
            continue;
         }

         /* locate the stream */
         for (s2ptr = LIST_GET_HEAD(&h2ptr->StreamList);
              s2ptr && s2ptr->Ident != ident;
              s2ptr = LIST_GET_NEXT(s2ptr));

         if (!s2ptr)
         {
            /* stream not found */
            if (ident < h2ptr->LastStreamIdent)
            {
               retval = Http2DoError (h2ptr, 0, HTTP2_ERROR_PROTOCOL, FI_LI);
               break;
            }
            /* generate a new stream */
            s2ptr = Http2StreamGet (h2ptr, ident);
         }

         if (s2ptr->StreamCancel)
         {
            /* just ignore cancelled streams */
            Http2StreamReset (h2ptr, s2ptr, HTTP2_ERROR_CLOSED, NULL, 0);
            count -= length;
            bptr += length;
            continue;
         }
      }

      switch (type)
      {
         case HTTP2_FRAME_DATA :
              if (s2ptr && s2ptr->StreamClosed)
              {
                 /* special case where data received for closed stream */
                 Http2StreamReset (h2ptr, s2ptr, HTTP2_ERROR_CLOSED, NULL, 0);
                 retval = 0;
                 break;
              }
              retval = Http2RequestData (h2ptr, s2ptr, flags, bptr, length);
              if (retval < 0) Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_HEADERS :
         case HTTP2_FRAME_CONTINUATION :
              retval = HpackHeadersFrame (s2ptr, type, flags, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_PRIORITY :
              retval = Http2Priority (h2ptr, ident, flags, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_RST_STREAM :
              retval = Http2StreamReset (h2ptr, s2ptr, 0, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_SETTINGS :
              retval = Http2Settings (h2ptr, ident, flags, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_PUSH_PROMISE :
              /* client cannot server-push (RFC7540 8.2) */
              retval = -HTTP2_ERROR_PROTOCOL;
              retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_PING :
              retval = Http2Ping (h2ptr, ident, flags, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_GOAWAY :
              retval = Http2GoAway (h2ptr, ident, 0, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         case HTTP2_FRAME_WINDOW_UPDATE :
              retval = Http2WindowUpdate (h2ptr, s2ptr, ident, 0, bptr, length);
              if (retval < 0)
                 retval = Http2DoError (h2ptr, ident, retval, FI_LI);
              break;

         default :
              /* just ignore any unknown frame type */
              if (h2ptr->HeaderThisIdent)
              {
                 /* except between header frames */
                 retval = Http2DoError (h2ptr, 0, HTTP2_ERROR_PROTOCOL, FI_LI);
                 break;
              }
              retval = 0;
      }

      /* if Http2DoError() was a fail connection */
      if (retval < 0)
      {
         Http2NetClientEnd (h2ptr, ident);
         return;
      }

      count -= length;
      bptr += length;
   }

   if (count)
   {
      /* shuffle remaining data to front of buffer */
      memcpy (h2ptr->ReadBufferPtr, bptr, count);
   }

   h2ptr->ReadBufferCount = count;
   bptr = h2ptr->ReadBufferPtr + count;
   length = h2ptr->ReadBufferSize - count;

   /* stop reading when the HTTP/2 connection is closing */
   if (h2ptr->ConnectClosing) return;

   h2ptr->ReadInUseCount++;

   /* get the next, or rest of, frame */
   NetIoRead (h2ptr->NetIoPtr, Http2NetClientReadAst, h2ptr, bptr, length);
}

/*****************************************************************************/
/*
Look for the stream ident and end it.
*/ 

void Http2NetClientEnd
(
HTTP2_STRUCT *h2ptr,
uint ident
)
{
   HTTP2_STREAM_STRUCT  *s2ptr;

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

   /* locate the request corresponding to the stream */
   for (s2ptr = LIST_GET_HEAD(&h2ptr->StreamList);
        s2ptr && s2ptr->Ident != ident;
        s2ptr = LIST_GET_NEXT(s2ptr));

   if (s2ptr) Http2StreamClose (s2ptr);
}                 

/*****************************************************************************/
/*
Flag as cancelled any outstanding request I/O (read and write).
*/

void Http2NetIoCancel (NETIO_STRUCT *ioptr)

{
   HTTP2_STREAM_STRUCT  *s2ptr;

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

   s2ptr = ioptr->Stream2Ptr;

   s2ptr->StreamCancel = true;

   if (s2ptr->RequestPtr) Http2NetCancelWrite (s2ptr->RequestPtr);
}

/*****************************************************************************/
/*
Return true if this HTTP/2 stream has outstanding network I/O.
*/ 

BOOL Http2NetIoInProgress (NETIO_STRUCT *ioptr)

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

   ioptr = ((HTTP2_STREAM_STRUCT*)ioptr->Stream2Ptr)->Http2Ptr->NetIoPtr;

   if (ioptr->SesolaPtr) return (SesolaNetIoInProgress (ioptr->SesolaPtr));

   return (ioptr->WriteAstFunction || ioptr->ReadAstFunction);
}                 

/*****************************************************************************/
/*
This function wraps the HTTP/1.1 network I/O response reads.  Named
Http2NetIo..() for consistency with NetIo..() and SesolaNetIo..().  It is
called from NetIoRead().  It calls the Http2RequestData() function to handle
the asynchronous HTTP/2 DATA frames from the client (request body) and the
reads by the request body processing code.
*/ 

int Http2NetIoRead (NETIO_STRUCT *ioptr)

{
   HTTP2_STRUCT  *h2ptr;
   HTTP2_STREAM_STRUCT  *s2ptr;
   REQUEST_STRUCT  *rqptr;

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

   s2ptr = ioptr->Stream2Ptr;
   h2ptr = s2ptr->Http2Ptr;
   rqptr = s2ptr->RequestPtr;

   if (WATCHMOD (rqptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_HTTP2,
                 "Http2NetRead() !&A !&X !UL",
                 ioptr->ReadAstFunction, ioptr->ReadPtr, ioptr->ReadSize);

   /* no such creature as a blocking HTTP/2 read! */
   if (!ioptr->ReadAstFunction)
   {
      ErrorNoticed (rqptr, SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      return (SS$_BUGCHECK);
   }

   return (Http2RequestData (h2ptr, s2ptr, 0, NULL, 0));
}                 

/*****************************************************************************/
/*
Wraps the HTTP/1.1 network I/O response writes in HTTP/2 DATA frames.  Named
Http2NetIo..() for consistency with NetIo..() and SesolaNetIO..().  It is
called from NetIoWrite().  The function must be able to queue multiple
(sub-)writes should a request write exceed the maximum client accepted frame
size.  Optimally the write buffers will always be sized at or below the maximum
frame size.

All HTTP/2 I/O are by nature asynchronous and so a blocking write (one with a
NULL |AstFunction| parameter) is made asynchronous.  Data persistance must be
guaranteed during the write and so is *always* copied to HTTP/2 internal
buffers and *never* used in-situ, as the calling code may well (*will*)
continue on processing and likely (*will*) modify the data buffer it considers
was *already* written.  An example of where blocking writes are used is the
WATCH facility.

If |DataPtr| is NULL then an empty frame with the end data flag set is sent. 
If the stream associated with the request has been closed then allow the write
to be queued but the status will be delivered as cancelled, not performing the
network I/O but allowing any AST to be delivered.
*/ 

int Http2NetIoWrite
(
NETIO_STRUCT *ioptr,
VOID_AST AstFunction,
void *AstParam,
void *DataPtr,
uint DataLength
)
{
   BOOL  cache, magic;
   int  queue, status;
   HTTP2_STRUCT  *h2ptr;
   HTTP2_HEADER_STRUCT  *hsptr;
   HTTP2_STREAM_STRUCT  *s2ptr;
   HTTP2_WRITE_STRUCT  *w2ptr;
   REQUEST_STRUCT  *rqptr;
   VM_STRUCT  *vmptr;

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

   s2ptr = ioptr->Stream2Ptr;
   h2ptr = s2ptr->Http2Ptr;
   rqptr = s2ptr->RequestPtr;

   if (WATCHMOD (rqptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_HTTP2,
                 "Http2NetIoWrite() !&A !&X !UL",
                 AstFunction, DataPtr, DataLength);

   if (!AstFunction)
   {
      /* blocking write */
      if (ioptr->VmsStatus)
      {
         ioptr->WriteIOsb.Count = 0;
         ioptr->WriteIOsb.Status = ioptr->VmsStatus;
         return (ioptr->WriteIOsb.Status);
      }
      ioptr->WriteIOsb.Count = DataLength;
      ioptr->WriteIOsb.Status = SS$_NORMAL;
   }

   if (rqptr->rqPathSet.Http2WriteQueue)
      queue = rqptr->rqPathSet.Http2WriteQueue;
   else
      queue = HTTP2_WRITE_QUEUE_NORMAL;

   if (!DataPtr)
   {
      DataPtr = "";
      DataLength = 0;
   }

   if (rqptr->rqCache.EntryPtr &&
       rqptr->rqCache.EntryPtr->InUseCount > 1)
   {
      cache = true;
      magic = false;
   }
   else
   {
      /* check for the appropriate incantation */
      vmptr = (VM_STRUCT*)((uchar*)DataPtr - sizeof(VM_STRUCT));
      switch (vmptr->magic)
      {
         case VM_MAGIC_CACHE :
         case VM_MAGIC_DECC :
         case VM_MAGIC_EXPAT :
         case VM_MAGIC_GENERAL :
         case VM_MAGIC_HTTP2 :
         case VM_MAGIC_OPENSSL :
         case VM_MAGIC_PERMCACHE :
         case VM_MAGIC_REQUEST : magic = true; break;
         default: magic = false;
      }
      cache = false;
   }

   if (Watch.TriggerTxCount)
   {
      if (h2ptr != Watch.Http2Ptr)
      {
         if (!ioptr->WatchTriggerRxPlus &&
             !ioptr->WatchTriggerTxPlus)
         {
            if (WatchTrigger (DataPtr, DataLength, Watch.TriggerTx))
            {
               ioptr->WatchTriggerTx = true;
               if (Watch.TriggerPlus) ioptr->WatchTriggerTxPlus = true;
            }
        }
        if (ioptr->WatchTriggerTx || ioptr->WatchTriggerTxPlus)
        {
           WatchSetTrigger (ioptr);
           ioptr->WatchTriggerTx = false;
        }
      }
   }

   for (;;)
   {
      /******************/
      /* write frame(s) */
      /******************/

#if WATCH_MOD
      /* loop for where a write potentially exceeds the max frame size */
      if (WATCHING (rqptr, WATCH_HTTP2))
         WatchThis (WATCHITM(rqptr), WATCH_HTTP2, "!AZ!AZ!AZ !UL/!UL",
                    magic ? "MAGIC" : "",
                    cache ? "CACHE" : "",
                    !(magic || cache) ? "BUFFER" : "",
                    DataLength, h2ptr->ClientMaxFrameSize);
#endif

      if (magic)
      {
         /* can use the in-built header of WASD allocated memory */
         w2ptr = Http2GetWriteStruct (h2ptr, 0, FI_LI);
         w2ptr->HeaderPtr = vmptr->h2header;
         w2ptr->DataPtr = DataPtr;
         if (DataLength <= h2ptr->ClientMaxFrameSize)
         {
            w2ptr->DataLength = DataLength;
            /* final or only write delivers any AST */
            w2ptr->AstFunction = AstFunction;
            w2ptr->AstParam = AstParam;
         }
         else
            w2ptr->DataLength = h2ptr->ClientMaxFrameSize;

         if (WATCHING (ioptr, WATCH_NETWORK_OCTETS))
            WatchDataDump (w2ptr->DataPtr, w2ptr->DataLength);

         hsptr = w2ptr->HeaderPtr;
         HTTP2_PLACE_24 (hsptr->length, w2ptr->DataLength)
         HTTP2_PLACE_8  (hsptr->type, HTTP2_FRAME_DATA);
         HTTP2_PLACE_8  (hsptr->flags, 0);
         HTTP2_PLACE_32 (hsptr->ident, rqptr->Stream2Ptr->Ident);
      }
      else
      if (cache)
      {
         /* handle cached file where magic is already in use */
         w2ptr = Http2GetWriteStruct (h2ptr, 0, FI_LI);
         w2ptr->HeaderPtr = w2ptr->header;
         w2ptr->DataPtr = DataPtr;
         if (DataLength <= h2ptr->ClientMaxFrameSize)
         {
            w2ptr->DataLength = DataLength;
            /* final or only write delivers any AST */
            w2ptr->AstFunction = AstFunction;
            w2ptr->AstParam = AstParam;
         }
         else
            w2ptr->DataLength = h2ptr->ClientMaxFrameSize;
      }
      else
      {
         /* buffer partial or "blocking" or non-allocated (magic) write */
         if (DataLength <= h2ptr->ClientMaxFrameSize)
            w2ptr = Http2GetWriteStruct (h2ptr, DataLength, FI_LI);
         else
            w2ptr = Http2GetWriteStruct (h2ptr, h2ptr->ClientMaxFrameSize, FI_LI);
         w2ptr->HeaderPtr = w2ptr->header;
         w2ptr->DataPtr = w2ptr->payload;
         if (DataLength <= h2ptr->ClientMaxFrameSize)
         {
            w2ptr->DataLength = DataLength;
            /* final or only write delivers any AST */
            w2ptr->AstFunction = AstFunction;
            w2ptr->AstParam = AstParam;
         }
         else
            w2ptr->DataLength = h2ptr->ClientMaxFrameSize;
         memcpy (w2ptr->DataPtr, DataPtr, w2ptr->DataLength);
      }

      w2ptr->Http2Ptr = h2ptr;
      w2ptr->RequestPtr = rqptr;
      w2ptr->WriteQueue = queue;

      HTTP2_PLACE_24 (w2ptr->length, w2ptr->DataLength)
      HTTP2_PLACE_8  (w2ptr->type, HTTP2_FRAME_DATA);
      HTTP2_PLACE_8  (w2ptr->flags, 0);
      HTTP2_PLACE_32 (w2ptr->ident, rqptr->Stream2Ptr->Ident);

      Http2NetQueueWrite (h2ptr, w2ptr);

      /* on the only or final write then that's it */
      if (DataLength <= h2ptr->ClientMaxFrameSize) return (SS$_NORMAL);

      /* if intermediate write then continue to the next */
      (uchar*)DataPtr += w2ptr->DataLength;
      DataLength -= w2ptr->DataLength;
      /* any magic cannot be used with subsequent writes */
      magic = false;
   }
}

/*****************************************************************************/
/*
HTTP/2 network writes are serialised using FIFO lists.  In this current
implementation of WASD HTTP/2 there is limited prioritisation of streams. 
There are four queues/lists.  Connection management writes are to queue zero
(index 0) and have the highest priority, then the high priority (index 1),
normal priority (index 2), and then the lowest (index 3).  The HIGH, NORMAL
(default) and LOW can be set via path mappings.  All writes from a queue are
FIFO and asynchronous (empty AST function notwithstanding).

Request data writes are subject to RFC 9113 (was 7540) flow control.  Where
write window size(s) become constrained individual DATA writes may fragmented
into multiple writes.  If a queued write is too large for the window size then
as much as can be written is, with the remainder of that data queued into
another, separate and independent write, which in turn can be partially written
and so on and so forth, until the remainder becomes small enough to be the last
fragment.
*/ 

void Http2NetQueueWrite
(
HTTP2_STRUCT *h2ptr,
HTTP2_WRITE_STRUCT *w2ptr
)
{
   int  cnt, idx, flow, status, type,
        DataLength;
   void  *AstParam,
         *DataPtr;
   REQUEST_STRUCT  *rqptr;
   HTTP2_STREAM_STRUCT  *s2ptr;
   HTTP2_WRITE_STRUCT  *w22ptr;
   VOID_AST  AstFunction;

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

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
                 "Http2NetQueueWrite() h2ptr:!&X w2ptr:!&X write-in-prog:!&B",
                 h2ptr, w2ptr, NETIO_WRITE_IN_PROGRESS(h2ptr->NetIoPtr));

   if (w2ptr)
   {
      /*************/
      /* new write */
      /*************/

      /* must always have an AST address to indicate NETIO is in use */
      if (!w2ptr->AstFunction)
         w2ptr->AstFunction = w2ptr->AstParam = Http2Net_WRITE_NO_AST;

      /* add to the HTTP/2 connection's write list */
      ListAddTail (&h2ptr->QueuedWriteList[w2ptr->WriteQueue], w2ptr,
                   LIST_ENTRY_TYPE_WRITE2);
      if (w2ptr->RequestPtr) w2ptr->RequestPtr->Stream2Ptr->QueuedWriteCount++;

      cnt = 0;
      for (idx = HTTP2_WRITE_QUEUE_MAX; idx <= HTTP2_WRITE_QUEUE_LOW; idx++)
         cnt += LIST_GET_COUNT (&h2ptr->QueuedWriteList[idx]);
      if (cnt > h2ptr->QueuedWritePeak) h2ptr->QueuedWritePeak = cnt;
   }

   /* if there's a write already in progress */
   if (NETIO_WRITE_IN_PROGRESS(h2ptr->NetIoPtr)) return;

   /***************/
   /* check queue */
   /***************/

   /* look through the queues from highest to lowest priority */
   for (idx = HTTP2_WRITE_QUEUE_MAX; idx <= HTTP2_WRITE_QUEUE_LOW; idx++)
   {
      /* if nothing in this queue then look at the next */
      if (!LIST_GET_COUNT (&h2ptr->QueuedWriteList[idx])) continue;

      for (w2ptr = LIST_GET_HEAD (&h2ptr->QueuedWriteList[idx]);
           w2ptr != NULL;
           w2ptr = w22ptr)
      {
         /* double check in case of some unexpected timing issue! */
         if (w2ptr->WriteInProgress) return;

         /* careful about pulling (flow control) mats */
         w22ptr = LIST_GET_NEXT(w2ptr);

         /* if not a DATA frame and therefore not subject to flow control */
         HTTP2_PEEK_8 (w2ptr->type, type);
         if (type != HTTP2_FRAME_DATA) break;

         /* if not impacting flow control */
         if (!w2ptr->DataLength) break;

         /* if not associated with a request */
         if (!(rqptr = w2ptr->RequestPtr)) break;

         /* if SS$_VCCLOSED or other specific status just do it */
         if (h2ptr->NetIoPtr->VmsStatus) break;

         if (!(s2ptr = rqptr->Stream2Ptr)) break;

         /* outstanding write for (in process of being) closed stream */
         if (s2ptr->StreamClosed) continue;

         /****************/
         /* flow control */
         /****************/

         /* if stream or connection is flow blocked look for another */
         if ((cnt = Http2FlowCheck (h2ptr, s2ptr, w2ptr)) <= 0)
         {
            if (cnt < 0) Http2StreamDrop (s2ptr);
            continue;
         }

         if (WATCHMOD (rqptr, WATCH_MOD_HTTP2))
            WatchThis (WATCHITM(rqptr), WATCH_MOD_HTTP2,
                       "FLOW length:!UL h2size:!SL s2size:!SL",
                       w2ptr->DataLength, h2ptr->WriteWindowSize,
                       s2ptr->WriteWindowSize); 

         /* adjust the flow control window in use */
         s2ptr->WriteWindowSize -= w2ptr->DataLength;
         h2ptr->WriteWindowSize -= w2ptr->DataLength;

         /* break to perform the write */
         break;
      }

      /* break from the queue loop to perform the write */
      if (w2ptr) break;
   }

   /* if nothing in any queue */
   if (idx > HTTP2_WRITE_QUEUE_LOW)
   {
      if (!h2ptr->NetIoPtr->Channel)
         Http2ConnectClose (h2ptr);
      else
      if (h2ptr->NetIoPtr->VmsStatus)
         Http2ConnectClose (h2ptr);
      else
      if (h2ptr->GoAwayLastStreamIdent)
         Http2ConnectClose (h2ptr);
      return;
   }

   /*********/
   /* write */
   /*********/

   w2ptr->WriteInProgress = true;

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
   {
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2, "!&X queue:!UL",
                 w2ptr, w2ptr->WriteQueue);
      Http2WatchFrame (h2ptr, w2ptr->HeaderPtr,
                       w2ptr->DataPtr, w2ptr->DataLength,
                       w2ptr->HeaderPtr != w2ptr->header);
   }
   else
   if (WATCHING (h2ptr, WATCH_HTTP2))
      Http2WatchFrame (h2ptr, w2ptr->HeaderPtr, w2ptr->DataPtr, 0,
                       w2ptr->HeaderPtr != w2ptr->header);

   /* if something is amiss with the HTTP/2 connection */
   if (h2ptr->NetIoPtr->VmsStatus)
   {
      NetIoWriteStatus (h2ptr->NetIoPtr, Http2NetWriteDataAst,
                        w2ptr, h2ptr->NetIoPtr->VmsStatus, 0);
      return;
   }

   if (rqptr = w2ptr->RequestPtr)
   {
      /* (RFC7540 5.1) */
      if (rqptr->NetIoPtr->VmsStatus)
      {
         /* SS$_NORMAL means no issue with the underlying HTTP/2 write */
         NetIoWriteStatus (h2ptr->NetIoPtr, Http2NetWriteDataAst,
                           w2ptr, SS$_NORMAL, 0);
         return;
      }
      h2ptr->FrameRequestCountTx++;
      h2ptr->FrameRequestTallyTx++;
   }

   h2ptr->FrameCountTx++;
   h2ptr->FrameTallyTx++;

   if ((uchar*)w2ptr->HeaderPtr + HTTP2_FRAME_HEADER_SIZE == w2ptr->DataPtr)
      /* header immediately precedes the data - write single datagram */
      NetIoWrite (h2ptr->NetIoPtr, Http2NetWriteDataAst, w2ptr,
                  w2ptr->HeaderPtr, HTTP2_FRAME_HEADER_SIZE +
                                    w2ptr->DataLength);
   else
      /* write header then data independently (two datagrams) */
      NetIoWrite (h2ptr->NetIoPtr, Http2NetWriteHeaderAst, w2ptr,
                  w2ptr->HeaderPtr, HTTP2_FRAME_HEADER_SIZE);
}

/*****************************************************************************/
/*
Deliver (all) write(s) associated with the request with an error status (most
commonly with WASD only the one).  Writes are delivered rather than cancelled
because some are associated with consumed resources (e.g. scripts) which need
to be released.
*/ 

BOOL Http2NetCancelWrite (REQUEST_STRUCT *rqptr)

{
   int  cnt, idx;
   HTTP2_STRUCT  *h2ptr;
   NETIO_STRUCT  *ioptr;
   HTTP2_WRITE_STRUCT  *w2ptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_HTTP2, "Http2NetCancelWrite()");

   h2ptr = rqptr->Http2Ptr;
   ioptr = h2ptr->NetIoPtr;

   /* IO struct is in use */
   if (ioptr->WriteAstFunction) return (true);

   /* look through the queues */
   for (idx = HTTP2_WRITE_QUEUE_MAX; idx <= HTTP2_WRITE_QUEUE_LOW; idx++)
   {
      /* if nothing in this queue then look at the next */
      if (!LIST_GET_COUNT (&h2ptr->QueuedWriteList[idx])) continue;

      for (w2ptr = LIST_GET_HEAD (&h2ptr->QueuedWriteList[idx]);
           w2ptr != NULL;
           w2ptr = LIST_GET_NEXT(w2ptr))
      {
         if (rqptr != w2ptr->RequestPtr) continue;
         if (w2ptr->WriteInProgress) return (false);

         if (WATCHING (h2ptr, WATCH_HTTP2))
            WatchThis (WATCHITM(rqptr), WATCH_HTTP2,
                       "CANCEL request !UL write", rqptr->ConnectNumber);

         w2ptr->WriteInProgress = true;
         NetIoWriteStatus (h2ptr->NetIoPtr, Http2NetWriteDataAst,
                           w2ptr, SS$_VCCLOSED, 0);
         return (true);
      }
   }

   return (false);
}

/*****************************************************************************/
/*
Post-process the HTTP/2 (9 octet) header write.  If OK then write the payload.
*/ 

void Http2NetWriteHeaderAst (HTTP2_WRITE_STRUCT *w2ptr)

{
   HTTP2_STRUCT  *h2ptr;
   NETIO_STRUCT  *io2ptr;

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

   h2ptr = w2ptr->Http2Ptr;
   io2ptr = h2ptr->NetIoPtr;

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
                 "Http2NetWriteHeaderAst() !&F !&X !&S !UL",
                 Http2NetWriteHeaderAst,
                 w2ptr, io2ptr->WriteStatus, io2ptr->WriteCount);

   /* if explicitly set (error) status */
   if (io2ptr->VmsStatus) io2ptr->WriteStatus = io2ptr->VmsStatus;

   if (VMSnok (io2ptr->WriteStatus))
   {
      /* the connection has failed (or at least the last write) */
      SysDclAst (Http2NetWriteDataAst, w2ptr);
      return;
   }

   h2ptr->BytesRawTx64 += io2ptr->WriteCount;
   h2ptr->BytesRawTallyTx64 += io2ptr->WriteCount;

   NetIoWrite (h2ptr->NetIoPtr, Http2NetWriteDataAst, w2ptr,
               w2ptr->DataPtr, w2ptr->DataLength);
}

/*****************************************************************************/
/*
Post-process the payload write (or combined header and payload write).
Delivers (any) AST.  Then queue the next write.
*/ 

void Http2NetWriteDataAst (HTTP2_WRITE_STRUCT *w2ptr)

{
   uint  type,
         WriteLength;
   void  *AstParam;
   HTTP2_STRUCT  *h2ptr;
   HTTP2_STREAM_STRUCT  *s2ptr;
   NETIO_STRUCT  *ioptr, *io2ptr;
   REQUEST_STRUCT  *rqptr;
   VOID_AST  AstFunction;

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

   h2ptr = w2ptr->Http2Ptr;
   io2ptr = h2ptr->NetIoPtr;

   if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
"Http2NetWriteDataAst() !&F h2ptr:!&X w2ptr:!&X !&S !UL", Http2NetWriteDataAst,
                 h2ptr, w2ptr, io2ptr->WriteStatus, io2ptr->WriteCount);

   /* if explicitly set (error) status */
   if (io2ptr->VmsStatus) io2ptr->WriteStatus = io2ptr->VmsStatus;

   h2ptr->BytesRawTx64 += io2ptr->WriteCount;
   h2ptr->BytesRawTallyTx64 += io2ptr->WriteCount;

   /* remove from the HTTP/2 connection's write list */
   ListRemove (&h2ptr->QueuedWriteList[w2ptr->WriteQueue], w2ptr);
   w2ptr->WriteInProgress = false;

   if (rqptr = w2ptr->RequestPtr)
   {
      /********************/
      /* request response */
      /********************/

      if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
         WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2,
                    "rqptr:!&X ast:!&A", rqptr, w2ptr->AstFunction);

      if (s2ptr = rqptr->Stream2Ptr)
         if (s2ptr->QueuedWriteCount)
            s2ptr->QueuedWriteCount--;

      ioptr = rqptr->NetIoPtr;

      /* keep the request accounting representative */
      ioptr->BlocksRawTx64++;
      ioptr->BlocksTallyTx64++;
      ioptr->BytesRawTx64 += io2ptr->WriteCount;
      ioptr->BytesTallyTx64 += io2ptr->WriteCount;

      if (w2ptr->AstFunction != Http2Net_WRITE_NO_AST)
      {
         /* deliver an AST so adjust the underlying NETIO */
         if (ioptr->VmsStatus)
         {
            /* use explicit request I/O status */
            ioptr->WriteIOsb.Count = 0;
            ioptr->WriteIOsb.Status = ioptr->VmsStatus;
         }
         else
         {
            /* use actual network I/O status */
            ioptr->WriteIOsb.Count = io2ptr->WriteCount;
            ioptr->WriteIOsb.Status = io2ptr->WriteStatus;
         }
         /* post-process the request I/O */
         ioptr->WriteCount = ioptr->WriteLength = 0;
         SysDclAst (NetIoWriteAst, ioptr);
      }
   }
   else
   {
      /**************************/
      /* HTTP/2 control message */
      /**************************/

      if (WATCHMOD (h2ptr, WATCH_MOD_HTTP2))
         WatchThis (WATCHITM(h2ptr), WATCH_MOD_HTTP2, "AstFunction !&A 0x!8XL",
                    w2ptr->AstFunction, w2ptr->AstParam);

      AstParam = w2ptr->AstParam;
      AstFunction = w2ptr->AstFunction;
      if (w2ptr->AstFunction != Http2Net_WRITE_NO_AST)
         AstFunction (AstParam);
   }

   Http2FreeWriteStruct (h2ptr, w2ptr, FI_LI);

   /********/
   /* next */
   /********/

   Http2NetQueueWrite (h2ptr, NULL);
}

/****************************************************************************/
/*
The presence of an AST function is used to indicate a NETIO in progress.  For
I/O environments that cannot block (viz. HTTP/2) this as an AST target can be
detected during post processing and not actually called.  Could have used a
magic number of some sort but this seemed cleaner.      
*/

void Http2Net_WRITE_NO_AST (void *param)

{
   if (WATCH_MODULE (WATCH_MOD_HTTP2))
      WatchThis (WATCHALL, WATCH_MOD_HTTP2,
                 "Http2Net_WRITE_NO_AST() !&F", Http2Net_WRITE_NO_AST);

   ErrorNoticed (NULL, SS$_BUGCHECK, "Http2Net_WRITE_NO_AST()", FI_LI);
}

/*****************************************************************************/
/*
Write an end-of-stream frame (at end of output) to the client.
This is the final element of the request's HTTP/2 data stream.
*/

void Http2NetWriteEnd (REQUEST_STRUCT *rqptr)

{
   int  queue;                      
   HTTP2_STRUCT  *h2ptr;
   HTTP2_WRITE_STRUCT  *w2ptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_HTTP2))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_HTTP2,
                 "Http2NetWriteEnd() !&F", Http2NetWriteEnd);

   h2ptr = rqptr->Http2Ptr;
   if (rqptr->rqPathSet.Http2WriteQueue)
      queue = rqptr->rqPathSet.Http2WriteQueue;
   else
      queue = HTTP2_WRITE_QUEUE_NORMAL;

   w2ptr = Http2GetWriteStruct (h2ptr, 0, FI_LI);

   HTTP2_PLACE_24 (w2ptr->length, 0);
   HTTP2_PLACE_8  (w2ptr->type, HTTP2_FRAME_DATA);
   HTTP2_PLACE_8  (w2ptr->flags, HTTP2_FLAG_END_STR);
   HTTP2_PLACE_32 (w2ptr->ident, rqptr->Stream2Ptr->Ident);

   w2ptr->Http2Ptr = h2ptr;
   w2ptr->HeaderPtr = w2ptr->header;
   w2ptr->DataPtr = w2ptr->payload;

   /* wind up the request */
   w2ptr->AstFunction = RequestEnd2;
   w2ptr->AstParam = rqptr;

   Http2NetQueueWrite (h2ptr, w2ptr);
}

/*****************************************************************************/
/*
Disconnect network connections.  Provides comparable HTTP/2 functionality to
NetControl().  Note that this is not an elegant "goaway". The network channel
is just cancelled running down any I/O.
*/ 

int Http2NetControl (int ConnectNumber)

{
   int  PurgeCount;
   HTTP2_STRUCT  *h2ptr, *w22ptr;

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

   if (WATCH_MODULE(WATCH_MOD_HTTP2))
      WatchThis (WATCHALL, WATCH_MOD_HTTP2, "Http2NetControl()");

   PurgeCount = 0;

   for (h2ptr = LIST_GET_HEAD(&Http2List); h2ptr != NULL; h2ptr = w22ptr)
   {
      /* get (any) next in list in case the current connection is closed */
      w22ptr = LIST_GET_NEXT(h2ptr);

      if (ConnectNumber <= -2)
      {
         /* purge all (including only HTTP2) */
         Http2GoAway (h2ptr, 0, HTTP2_ERROR_NONE, NULL, 0);
         Http2ConnectClose (h2ptr);
         PurgeCount++;
      }
      else
      if (ConnectNumber == h2ptr->ConnectNumber)
      {
         /* purge matching */
         Http2GoAway (h2ptr, 0, HTTP2_ERROR_NONE, NULL, 0);
         Http2ConnectClose (h2ptr);
         PurgeCount++;
      }
      else
      if (LIST_IS_EMPTY (&h2ptr->StreamList))
      {
         /* purge idle */
         Http2GoAway (h2ptr, 0, HTTP2_ERROR_NONE, NULL, 0);
         Http2ConnectClose (h2ptr);
         PurgeCount++;
      }
   }

   if (WATCH_MODULE(WATCH_MOD_HTTP2))
      WatchThis (WATCHALL, WATCH_MOD_HTTP2, "purged: !UL", PurgeCount);

   return (PurgeCount);
}

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

