/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                                  ws_echo.c

A demonstrator for CGIplus Web Socket scripts.

The JavaScript-driven client-side of this demonstrator is in

  WASD_ROOT:[SRC.WEBSOCKET]WS_ECHO.HTML

An example of using, and test-bench for, the string descriptor versions of the
wsLIB (WebSocket library) API.

Experiment with the WsLibOnNextRequest() approach to request synchronisation by
defining the logical name WS_ECHO_ON_NEXT_REQUEST.


COPYRIGHT
---------
Copyright (C) 2010-2021 Mark G.Daniel

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


VERSION HISTORY
---------------
18-AUG-2014  MGD  v1.2.0, demonstrate WsLibOnNextRequest()
21-JUL-2012  MGD  v1.0.1, WsLibDestroy() deprecated
07-AUG-2010  MGD  v1.0.0, initial development
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#define SOFTWAREVN "1.2.0"
#define SOFTWARENM "WS_ECHO"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#include <descrip.h>
#include <iodef.h>
#include <ssdef.h>
#include <stsdef.h>
#include <starlet.h>

#include "wslib.h"

/* from libmsg.h */
#define LIB$_INVSTRDES 1409572

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define Debug 0

#define DEFAULT_UNUSED_TIMEOUT 60

#define FI_LI "WS_ECHO", __LINE__
#define EXIT_FI_LI(status) { printf ("[%s:%d]", FI_LI); exit(status); }

int  ConnectedCount,
     UnusedTimeout = DEFAULT_UNUSED_TIMEOUT,
     UsageCount;

struct EchoClient {

   int  CharCount,
        CloseFrame,
        LineCount,
        PerKeyStroke;

   unsigned int  ConnectTime,
                 UnusedTime;

   char  InputBuffer [256],
         RemoteHost [128],
         ServerName [128],
         StatusBuffer [256];

   struct WsLibStruct  *WsLibPtr;

   struct dsc$descriptor_s  InputDsc,
                            StatusDsc;
};

/* function prototypes */
void AddClient ();
void EchoThis (struct WsLibStruct*);
void HeartBeat ();
int HtmlEscapeDsc (struct dsc$descriptor_s*, int);
void NextRequest ();
void RemoveClient (struct WsLibStruct *wsptr);

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

main ()

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

   /* don't want the C-RTL fiddling with the carriage control */
   stdout = freopen ("SYS$OUTPUT", "w", stdout, "ctx=bin");

   WsLibInit ();

   /* no clients is two minutes in seconds */
   WsLibSetLifeSecs (2*60);

   if (!WsLibIsCgiPlus())
   {
      fprintf (stdout, "Status: 500\r\n\r\n\
WebSocket needs to be CGIplus!\n");
   }
   else
   if (getenv("WS_ECHO_ON_NEXT_REQUEST") != NULL)
   {
      WsLibOnNextRequest (NextRequest);
      /* asynchronous from here-on-in */
      for (;;) sys$hiber();
   }
   else
   {
      for (;;)
      {
         WsLibCgiVar ("");

         sys$setast (0);

         UsageCount++;

         AddClient (0);

         WsLibCgiPlusEof ();

         sys$setast (1);
      }
   }

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Asynchronous notification the next request is available.  This function is
called at AST-delivery level and so no need to disable ASTs.  Add the client
and then notify the server we're ready for another.
*/

void NextRequest ()

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

   UsageCount++;

   AddClient (1);

   WsLibCgiPlusEof ();
}

/*****************************************************************************/
/*
Allocate a client structure and add it to the head of the list.
*/

void AddClient (int OnNext)

{
   static $DESCRIPTOR (GreetingFaoDsc, "!AZHello !AZ;&nbsp; !AZ at !AZ");
   static $DESCRIPTOR (OnNextDsc, "OnNext\7");

   int  len, status;
   unsigned int  CurrentTime;
   char  *sptr, *zptr;
   char  Greeting [256];
   struct EchoClient  *clptr;

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

   CurrentTime = WsLibTime ();

   clptr = calloc (1, sizeof(struct EchoClient));
   if (!clptr) EXIT_FI_LI (vaxc$errno);

   strcpy (clptr->ServerName, WsLibCgiVar("SERVER_NAME"));
   strcpy (clptr->RemoteHost, WsLibCgiVar("REMOTE_HOST"));
   clptr->ConnectTime = CurrentTime;
   clptr->UnusedTime = CurrentTime + UnusedTimeout;

   clptr->InputDsc.dsc$b_class = DSC$K_CLASS_S;
   clptr->InputDsc.dsc$b_dtype = DSC$K_DTYPE_T;

   clptr->StatusDsc.dsc$b_class = DSC$K_CLASS_S;
   clptr->StatusDsc.dsc$b_dtype = DSC$K_DTYPE_T;
   clptr->StatusDsc.dsc$a_pointer = clptr->StatusBuffer;

   /* create a WebSocket library structure for the client */
   if (!(clptr->WsLibPtr = WsLibCreate (clptr, RemoveClient)))
   {
      /* failed, commonly on some WebSocket protocol issue */
      free (clptr);
      return;
   }

   /* no user interaction is whatever's defined in seconds */
   WsLibSetIdleSecs (clptr->WsLibPtr, DEFAULT_UNUSED_TIMEOUT);

   /* open the IPC to the WebSocket (mailboxes) */
   status = WsLibOpen (clptr->WsLibPtr);
   if (VMSnok(status)) EXIT_FI_LI(status);

   /* if asynchronous next-request convey this to the JavaScript client */
   if (OnNext) WsLibWriteDsc (clptr->WsLibPtr, &OnNextDsc, WSLIB_ASYNCH);

   clptr->StatusDsc.dsc$w_length = sizeof(clptr->StatusBuffer);
   sys$fao (&GreetingFaoDsc, &clptr->StatusDsc.dsc$w_length,
            &clptr->StatusDsc, "\x07", clptr->RemoteHost,
            SOFTWAREID, clptr->ServerName);
   WsLibWriteDsc (clptr->WsLibPtr, &clptr->StatusDsc, WSLIB_ASYNCH);

   WsLibSetWakeCallback (clptr->WsLibPtr, HeartBeat, 1);

   /* queue an asynchronous read from the client */
   clptr->InputDsc.dsc$a_pointer = clptr->InputBuffer;
   clptr->InputDsc.dsc$w_length = sizeof(clptr->InputBuffer);
   WsLibReadDsc (clptr->WsLibPtr,
                 &clptr->InputDsc,
                 &clptr->InputDsc,
                 EchoThis);

   ConnectedCount++;
}

/*****************************************************************************/
/*
Remove the client structure from the list and free the memory.
*/

void RemoveClient (struct WsLibStruct *wsptr)

{
   struct EchoClient  *clptr;

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

   clptr = WsLibGetUserData (wsptr);

   free (clptr);

   if (ConnectedCount) ConnectedCount--;
}

/*****************************************************************************/
/*
Asynchronous read from a WebSocket client has concluded.
Echo the string.  Deliberately performed using three separate writes just to
illustrate the asynchronous and granular nature of WebSockets.
*/

void EchoThis (struct WsLibStruct *wsptr)

{
   static $DESCRIPTOR (CountFaoDsc, "!UL: ");
   static $DESCRIPTOR (NewLineDsc, "\n");

   int  len;
   char  *cptr, *sptr, *zptr;
   char  CountBuffer [32];
   $DESCRIPTOR (CountDsc, CountBuffer);
   struct EchoClient  *clptr;

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

   if (VMSnok (WsLibReadStatus(wsptr)))
   {
      /* WEBSOCKET_INPUT read error (can be EOF) */
      WsLibClose (wsptr, 0, NULL);
      return;
   }

   clptr = WsLibGetUserData(wsptr);

   clptr->UnusedTime = WsLibTime() + UnusedTimeout;

   /* CAUTION! with multiple concurrent write status becomes unreliable */
   if (clptr->InputDsc.dsc$w_length &&
       clptr->InputDsc.dsc$a_pointer[0] == 0x07)
   {
      /*****************/
      /* per keystroke */
      /*****************/

      clptr->InputDsc.dsc$a_pointer++;
      clptr->InputDsc.dsc$w_length--;

      /* dif'rent strokes for dif'rent browsers (prob'ly my fault) */
      if (clptr->InputDsc.dsc$a_pointer[0] == 0 ||
          clptr->InputDsc.dsc$a_pointer[0] == 10 ||
          clptr->InputDsc.dsc$a_pointer[0] == 13)
      {
         if (clptr->CharCount)
            WsLibWriteDsc (clptr->WsLibPtr, &NewLineDsc, WSLIB_ASYNCH);
         clptr->PerKeyStroke = clptr->CharCount = 0;
      }
      else
      {
         if (!clptr->PerKeyStroke)
         {
            clptr->PerKeyStroke = 1;
            sys$fao (&CountFaoDsc, &CountDsc.dsc$w_length, &CountDsc,
                     ++clptr->LineCount);
            /* deliberately (test-bench) blocking! */
            WsLibWriteDsc (clptr->WsLibPtr, &CountDsc, NULL);
         }
         clptr->CharCount++;
         HtmlEscapeDsc (&clptr->InputDsc, sizeof(clptr->InputBuffer));
         WsLibWriteDsc (clptr->WsLibPtr, &clptr->InputDsc, WSLIB_ASYNCH);
      }
   }
   else
   if (clptr->InputDsc.dsc$w_length)
   {
      /************/
      /* per line */
      /************/

      if (clptr->PerKeyStroke)
      {
         clptr->PerKeyStroke = clptr->CharCount = 0;
         WsLibWriteDsc (clptr->WsLibPtr, &NewLineDsc, WSLIB_ASYNCH);
      }

      sys$fao (&CountFaoDsc, &CountDsc.dsc$w_length, &CountDsc,
               ++clptr->LineCount);
      /* deliberately (test-bench) blocking! */
      WsLibWriteDsc (clptr->WsLibPtr, &CountDsc, NULL);

      HtmlEscapeDsc (&clptr->InputDsc, sizeof(clptr->InputBuffer));
      WsLibWriteDsc (clptr->WsLibPtr, &clptr->InputDsc, NULL);

      WsLibWriteDsc (clptr->WsLibPtr, &NewLineDsc, WSLIB_ASYNCH);
   }

   /* queue the next asynchronous read from the client */
   clptr->InputDsc.dsc$a_pointer = clptr->InputBuffer;
   clptr->InputDsc.dsc$w_length = sizeof(clptr->InputBuffer);
   WsLibReadDsc (clptr->WsLibPtr,
                 &clptr->InputDsc,
                 &clptr->InputDsc,
                 EchoThis);
}

/*****************************************************************************/
/*
Called by wsLIB every second to provide status information to each client.
*/

void HeartBeat (struct WsLibStruct *wsptr)

{
   static $DESCRIPTOR (StatusFaoDsc,
"!AZHello !AZ;&nbsp; Connected: !ULS;&nbsp; Timeout: !ULS;&nbsp; \
Connected: !UL;&nbsp; Usage: !UL");

   int  len, status;
   unsigned int  CurrentTime;
   char  StatusBuffer [256]; 
   $DESCRIPTOR (StatusDsc, StatusBuffer);
   struct EchoClient  *clptr;

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

   CurrentTime = WsLibTime ();

   clptr = WsLibGetUserData (wsptr);

   clptr->StatusDsc.dsc$w_length = sizeof(clptr->StatusBuffer);

   sys$fao (&StatusFaoDsc,
            &clptr->StatusDsc.dsc$w_length,
            &clptr->StatusDsc,
            "\x07", clptr->RemoteHost,
            CurrentTime - clptr->ConnectTime,
            clptr->UnusedTime - CurrentTime,
            ConnectedCount, UsageCount);

   WsLibWriteDsc (wsptr, &clptr->StatusDsc, WSLIB_ASYNCH);
}

/*****************************************************************************/
/*
Escape HTML-reserved characters.  Does this in-situ!  Provided there is enough
trailing storage.  If not the escaped string gets truncated in various (safe)
ways.
*/

int HtmlEscapeDsc
(
struct dsc$descriptor_s *StringDsc,
int SizeOfString
)
{
   int  extralen;
   char  *cptr, *czptr, *sptr, *zptr;

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

   if (StringDsc->dsc$b_class != DSC$K_CLASS_S &&
       StringDsc->dsc$b_dtype != DSC$K_DTYPE_T) return (LIB$_INVSTRDES);

   czptr = (cptr = StringDsc->dsc$a_pointer) + StringDsc->dsc$w_length;
   extralen = 0;
   while (cptr < czptr)
   {
      switch (*cptr)
      {
         case '<' : extralen += 3; break;
         case '>' : extralen += 3; break;
         case '&' : extralen += 4; break;
      }
      cptr++;
   }
   if (!extralen) return (SS$_NORMAL);

   zptr = StringDsc->dsc$a_pointer + SizeOfString - 1;
   sptr = cptr + extralen;
   if (sptr < zptr)
      *(unsigned short*)sptr = '\0\0';
   else
      *zptr = '\0';  
   while (cptr >= StringDsc->dsc$a_pointer)
   {
      switch (*cptr)
      {
         case '<' : sptr -= 3; if (sptr<zptr-4) memcpy(sptr,"&lt;",4); break;
         case '>' : sptr -= 3; if (sptr<zptr-4) memcpy(sptr,"&gt;",4); break;
         case '&' : sptr -= 4; if (sptr<zptr-5) memcpy(sptr,"&amp;",5); break;
         default: if (sptr < zptr) *sptr = *cptr;
      }
      sptr--;
      cptr--;
   }
   while (*sptr) sptr++;
   StringDsc->dsc$w_length = sptr - StringDsc->dsc$a_pointer;

   return (SS$_NORMAL);
}

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

