/*****************************************************************************/
/*
                                ProxyRework.c

The proxy rework facility will modify a target string to a replacement string
in the request header (e.g. Host:), the response header (e.g. set-cookie:), and
in the response body.  Rework will be applied to HTML and CSS responses.

These are simple string matches.


CONFIGURATION
-------------
The proxy rework facility must be enabled for a service by setting a maximum
size for the HTML response body to be reworked, in kB.

  # WASD_CONFIG_SERVICE
  [[*.1924]]
  [ServiceReworkMax]  128

Specific paths must then be SET in WASD_CONFIG_MAP to have proxy requests
reworked.

  # WASD_CONFIG_MAP
  [[*:1924]]
  set * proxy=rework=192.168.1.3=192.168.1.2


VERSION HISTORY
---------------
26-FEB-2021  MGD  initial
*/
/*****************************************************************************/

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

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

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

#define WASD_MODULE "PROXYREWORK"

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

int  ProxyReworkMax;

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

extern PROXY_ACCOUNTING_STRUCT  *ProxyAccountingPtr;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initialise the buffering of a response to be reworked.  Then manage that
buffering as required during the response.  Return |true| to continue, |false|
not to.
*/

BOOL ProxyReworkResponse (PROXY_TASK *tkptr)

{
   int  count, idx, size;
   char  *cptr, *ctptr;
   REQUEST_STRUCT  *rqptr;

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

   rqptr = tkptr->RequestPtr;

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY,
                 "ProxyResponseRework() !UL !UL",
                 tkptr->ResponseBodyLength, tkptr->ResponseContentLength);

   if (!tkptr->ResponseReworkInit)
   {
      /*********************/
      /* initialise rework */
      /*********************/

      tkptr->ResponseReworkInit = true;

      ProxyAccountingPtr->ReworkCount++;

      if (!tkptr->ResponseContentTypePtr)
      {
         /* no content-type in proxied HTTP response - look for a file type */
         for (cptr = rqptr->rqHeader.PathInfoPtr; *cptr; cptr++);
         while (cptr > rqptr->rqHeader.PathInfoPtr &&
                *cptr != '/' && *cptr != '.') cptr--;
         if (*cptr == '.' && *(cptr+1))
         {
            /* a file extension (perhaps) - try to map to content type */
            ctptr = ConfigContentType (NULL, cptr);
            if (WATCHING (tkptr, WATCH_PROXY_REWORK))
               WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "TYPE !AZ !AZ",
                          cptr, ctptr);
            tkptr->ResponseContentTypePtr = ctptr;
         }
      }

      if (!tkptr->ResponseContentTypePtr ||
          MATCH9(tkptr->ResponseContentTypePtr, "text/html") ||
          MATCH8(tkptr->ResponseContentTypePtr, "text/css"))
      {
         /* unknown (we'll sniff) or reworkable content */
         tkptr->ResponseReworkMax = rqptr->ServicePtr->ProxyReworkMax;
         ProxyReworkBuffer (tkptr);
      }
      else
         return (true);

      /* initialise the host1=host2 data */
      size = strlen(rqptr->rqPathSet.ProxyReworkPtr)+1;
      cptr = VmGetHeap (rqptr, size);
      strzcpy (cptr, rqptr->rqPathSet.ProxyReworkPtr, size);
      for (idx = 0; idx < 4 && *cptr; idx++)
      {
         tkptr->ReworkPtr1[idx] = cptr;
         while (*cptr && *cptr != '=') cptr++;
         tkptr->ReworkLen1[idx] = cptr - tkptr->ReworkPtr1[idx];
         if (*cptr) *cptr++ = '\0';
         for (tkptr->ReworkPtr2[idx] = cptr; *cptr && *cptr != '$'; cptr++);
         tkptr->ReworkLen2[idx] = cptr - tkptr->ReworkPtr2[idx];
         if (*cptr) *cptr++ = '\0';

         if (WATCHING (tkptr, WATCH_PROXY_REWORK))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                       "REPLACE !&Z with !&Z",
                       tkptr->ReworkPtr1[idx], tkptr->ReworkPtr2[idx]);
      }
   }

   if (tkptr->ResponseBufferSize > tkptr->ResponseReworkMax * 1024)
   {
      /***********/
      /* too big */
      /***********/

      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                    "REWORK abort !UL (!UL) exceeds !UL kB",
                    tkptr->ResponseBufferSize / 1024,
                    tkptr->ResponseBufferSize,
                    tkptr->ResponseReworkMax);

      ProxyAccountingPtr->ReworkTooBig++;
      tkptr->ResponseStatusCode = 500;
      ErrorGeneralOverflow (rqptr, FI_LI);
      return (false);
   }

   if (!tkptr->ResponseContentTypePtr)
   {
      /************************/
      /* content type unknown */
      /************************/

      if (tkptr->ResponseBodyLength)
      {
         ProxyAccountingPtr->ReworkNoType++;

         /* we (perhaps now) have some body to sniff */
         ProxyReworkSniffHtml (tkptr);

         if (!tkptr->ResponseContentTypePtr)
         {
            /***************/
            /* quit rework */
            /***************/

            if (WATCHING (tkptr, WATCH_PROXY_REWORK))
               WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                          "NO distinctive ODOUR");

            /* not reworkable so go back to just proxying the content */
            tkptr->ResponseReworkMax = 0;
            tkptr->ResponseBufferNetPtr = tkptr->ResponseBufferPtr;
            tkptr->ResponseBufferNetCount = tkptr->ResponseBodyLength;
            ProxyResponseNetWrite (tkptr);
            return (true);
         }
      }
   }

   count = tkptr->ResponseBufferCurrentPtr - tkptr->ResponseBufferPtr;
   if (tkptr->ResponseBufferSize - count < rqptr->NetIoPtr->TcpMaxQio / 2)
   {
      /*****************/
      /* expand buffer */
      /*****************/

      tkptr->ResponseBufferSize += rqptr->NetIoPtr->TcpMaxQio;
      tkptr->ResponseBufferPtr =
         VmReallocHeap (rqptr, tkptr->ResponseBufferPtr,
                        tkptr->ResponseBufferSize, FI_LI);
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr + count;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize - count;
   }

   return (true);
} 

/*****************************************************************************/
/*
Adjust the network buffer as required.
*/

void ProxyReworkBuffer (PROXY_TASK *tkptr)

{
   int  len;
   char  *cbptr, *cptr, *ctptr;
   REQUEST_STRUCT  *rqptr;

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

   rqptr = tkptr->RequestPtr;

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

   if (tkptr->ResponseContentLength)
   {
      /* response has specified a content length */
      if (tkptr->ResponseContentLength / 1024 > tkptr->ResponseReworkMax)
      {
         /* exceeds maximum size - will be reported by caller */
         tkptr->ResponseBufferSize = tkptr->ResponseContentLength;
         return;
      }
   }

   if (tkptr->ResponseContentLength > tkptr->ResponseBufferSize)
   {
      /* allocate buffer based on specified content-length */
      cbptr = tkptr->ResponseBufferPtr;
      tkptr->ResponseBufferSize = tkptr->ResponseContentLength;
      tkptr->ResponseBufferPtr = VmGetHeap (rqptr, tkptr->ResponseBufferSize);
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize;
   }
   else
   {
      /* continue to use the existing buffer and grow as required */
      cbptr = NULL;
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize;
   }

   if (WATCHING (tkptr, WATCH_PROXY_REWORK))
      WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "REWORK !UL kB",
                 tkptr->ResponseReworkMax);

   if (tkptr->ResponseBufferNetCount)
   {
      /* response body exists beyond the response header */
      memcpy (tkptr->ResponseBufferPtr,
              tkptr->ResponseBufferNetPtr,
              tkptr->ResponseBufferNetCount);
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr +
                                        tkptr->ResponseBufferNetCount;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize -
                                       tkptr->ResponseBufferNetCount;
      tkptr->ResponseBodyLength = tkptr->ResponseBufferNetCount;
   }
   else
   {
      /* use the entire buffer */
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize;
      tkptr->ResponseBodyLength = 0;
   }

   if (cbptr)
   {
      /* dispose of the previous buffer */
      VmFreeFromHeap (rqptr, cbptr, FI_LI);
   }
} 

/*****************************************************************************/
/*
The response header and body have been read and buffered.  If the content-type
is unknown (no header field) then first check if the request path had a file
type (e.g. ".html") and try to determine the content-type from that.  If it
didn't then sniff the body for indications of HTML or CSS.  If the body is
suitable to rework peform that and return the reworked content.  If not just
return the body as-is.
*/

void ProxyReworkProcess (PROXY_TASK *tkptr)

{
   int  cnt, delta, idx, len, rblen;
   char  *cptr, *ctptr;
   REQUEST_STRUCT  *rqptr;

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

   rqptr = tkptr->RequestPtr;

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

   ProxyReworkHeader (tkptr);

   for (idx = 0; idx < 4; idx++)
   {
      if (!tkptr->ReworkPtr1[idx]) break;

      ProxyAccountingPtr->ReworkReplaceSearch++;

      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                    "BODY replacing !&Z with !&Z",
                    tkptr->ReworkPtr1[idx], tkptr->ReworkPtr2[idx]);

      rblen = tkptr->ResponseBodyLength;

      len = StringReplace (rqptr, &tkptr->ResponseBufferPtr,
                           tkptr->ReworkPtr1[idx], tkptr->ReworkPtr2[idx]);

      if (len)
      {
         tkptr->ResponseBodyLength = len;
         delta = tkptr->ReworkLen1[idx] - tkptr->ReworkLen2[idx];
         cnt = (len - rblen) / delta;
         if (cnt < 0) cnt = -cnt;
         if (WATCHMOD (rqptr, WATCH_MOD_PROXY))
            WatchThis (WATCHITM(rqptr), WATCH_MOD_PROXY, "!SL !UL-!UL=!SL !UL",
                       delta, len, rblen, len-rblen, cnt);
         if (WATCHING (tkptr, WATCH_PROXY_REWORK))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                       "BODY !UL replaced", cnt);
         ProxyAccountingPtr->ReworkReplaceCount += cnt;
      }
      else
      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "BODY 0 replaced");
   }

   if (WATCHMOD (tkptr, WATCH_MOD_PROXY))
   {
      if ((len = strlen(tkptr->ResponseContentTypePtr)) > 15) len = 15;
      WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY, "!#AZ !UL !UL !UL",
                 len, tkptr->ResponseContentTypePtr,
                 tkptr->ResponseBufferSize,
                 tkptr->ResponseBodyLength,
                 tkptr->ResponseContentLength);
      WatchDataDump (tkptr->ResponseBufferPtr, tkptr->ResponseBodyLength);
   }

   tkptr->ResponseReworkMax = 0;

   /* ..ResponseBufferNet.. specify what is sent to the client */
   tkptr->ResponseBufferNetPtr = tkptr->ResponseBufferPtr;
   tkptr->ResponseBufferNetCount = tkptr->ResponseBodyLength;
} 

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

void ProxyReworkWriteBody (REQUEST_STRUCT *rqptr)

{
   PROXY_TASK  *tkptr;

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

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

   tkptr = rqptr->ProxyTaskPtr;

   NetWrite (rqptr, &ProxyReworkEnd,
             tkptr->ResponseBufferPtr, tkptr->ResponseBodyLength);
} 

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

void ProxyReworkEnd (REQUEST_STRUCT *rqptr)

{
   PROXY_TASK  *tkptr;

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

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

   tkptr = rqptr->ProxyTaskPtr;

   ProxyEnd (tkptr);
} 

/*****************************************************************************/
/*
The response did not specify a content-type so sniffing the content is there
any odour of HTML.  Every response should have a content-type!  But...
fortunately HTML is fairly easily identified.  Or should be.
*/

BOOL ProxyReworkSniffHtml (PROXY_TASK *tkptr)

{
   BOOL  yep = false;
   char  ch = 0;
   char  *cptr;

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

   if (WATCHING (tkptr, WATCH_PROXY_REWORK))
      WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "SNIFF for HTML");

   for (cptr = tkptr->ResponseBufferPtr; isspace(*cptr); cptr++);

   if (!memcmp (cptr, "<!DOCTYPE html", 14))
      yep = true;
   else
   {
      /* confine the search */
      if (tkptr->ResponseBodyLength > 256)
         if (tkptr->ResponseBufferPtr[255])
            ch = tkptr->ResponseBufferPtr[255];

      if (strcasestr (cptr, "<html") &&
          strcasestr (cptr, "<head") &&
          strcasestr (cptr, "<title")) yep = true;

      if (ch) tkptr->ResponseBufferPtr[255] = '\0';
   }

   if (yep)
   {
      tkptr->ResponseContentTypePtr = "text/html";
      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "SMELLS of HTML");
   }
   return (yep);
}

/*****************************************************************************/
/*
Rework strings in the (rebuilt) response header.
*/

void ProxyReworkHeader (PROXY_TASK *tkptr)

{
   int  cnt, delta, idx, len, rblen;
   REQUEST_STRUCT  *rqptr;

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

   rqptr = tkptr->RequestPtr;

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

   for (idx = 0; idx < 4; idx++)
   {
      if (!tkptr->ReworkPtr1[idx]) break;

      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                    "HEADER replacing !&Z with !&Z",
                    tkptr->ReworkPtr1[idx], tkptr->ReworkPtr2[idx]);

      rblen = tkptr->RebuiltHeaderLength;

      len = StringReplace (rqptr, &tkptr->RebuiltHeaderPtr,
                           tkptr->ReworkPtr1[idx], tkptr->ReworkPtr2[idx]);

      if (len)
      {
         /* see ProxyReworkProcess() for detail */
         tkptr->RebuiltHeaderLength = len;
         delta = tkptr->ReworkLen1[idx] - tkptr->ReworkLen2[idx];
         cnt = (len - rblen) / delta;
         if (cnt < 0) cnt = -cnt;
         if (WATCHING (tkptr, WATCH_PROXY_REWORK))
            WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK,
                       "HEADER !UL replaced", cnt);
         ProxyAccountingPtr->ReworkReplaceCount += cnt;
      }
      else
      if (WATCHING (tkptr, WATCH_PROXY_REWORK))
         WatchThis (WATCHITM(tkptr), WATCH_PROXY_REWORK, "HEADER 0 replaced");
   }
}

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

