/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                                  ws_chat.c

(What must be a "classic") demonstrator for CGIplus Web Socket scripts.

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

  WASD_ROOT:[SRC.WEBSOCKET]WS_CHAT.HTML

Suppress host name lookup by defining the logical name WS_CHAT_NO_LOOKUP.

Experiment with the WsLibOnNextRequest() approach to request synchronisation by
defining the logical name WS_CHAT_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.1.1, WsLibDestroy() deprecated
03-DEC-2011  MGD  v1.1.0, '?' for list of participants
                          RemoteHostName() for host name
07-AUG-2010  MGD  v1.0.0, initial development
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#define SOFTWAREVN "1.2.0"
#define SOFTWARENM "WS_CHAT"
#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 <netdb.h>

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

#include "wslib.h"

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

#define Debug 0

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

#define DEFAULT_UNUSED_TIMEOUT 300  /* seconds */

int  ConnectedCount,
     UnusedTimeout = DEFAULT_UNUSED_TIMEOUT,
     UsageCount;

unsigned int  CurrentTime;
unsigned long  CurrentBinTime [2];

char  ServerName [128];

struct ChatClient {

   int  CloseFrame,
        UpdateCount,
        YouIdent;

   unsigned int  UnusedTime;

   char  ChatBuffer [2048],
         ConnectTime [32],
         Handle [64],
         InputBuffer [128],
         RemoteHost [128];

   struct WsLibStruct  *WsLibPtr;
};

/* function prototypes */
void AddClient ();
void EchoChat (struct ChatClient*);
int HtmlEscape (char*, int);
void NextRequest ();
void RemoteHostName (struct ChatClient*);
void RemoveClient (struct WsLibStruct *wsptr);
void UpdateClient (struct WsLibStruct*);

/*****************************************************************************/
/*
AST delivery is disabled during client acceptance and the add-client function
is deferred using an AST to help minimise the client setup window with a
potentially busy WebSocket application.
*/

main ()

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

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

   /* 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_CHAT_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)

{
   int  cnt, len, status;
   char  *sptr, *zptr;
   char  ConnectTime [32],
         Greeting [2048],
         YouString [32];
   $DESCRIPTOR (TimeFaoDsc, "!20%D\0");
   $DESCRIPTOR (TimeDsc, "");
   struct WsLibStruct  *wsptr;
   struct ChatClient  *clptr, *cl2ptr;

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

   if (!ServerName[0]) strcpy (ServerName, WsLibCgiVar ("SERVER_NAME"));

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

   /* won't have this until we have a request */
   RemoteHostName (clptr);
   TimeDsc.dsc$a_pointer = clptr->ConnectTime;
   TimeDsc.dsc$w_length = sizeof(clptr->ConnectTime);
   sys$fao (&TimeFaoDsc, 0, &TimeDsc, 0);
   clptr->YouIdent = UsageCount;
   clptr->UnusedTime = CurrentTime + UnusedTimeout;

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

   /* no user interaction is two minutes in seconds */
   WsLibSetIdleSecs (clptr->WsLibPtr, 2*60);

   /* 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) WsLibWrite (clptr->WsLibPtr, "OnNext", 6, WSLIB_ASYNCH);

   /* tell the client JavaScript which data is its */
   len = sprintf (YouString, "you=%u", clptr->YouIdent);
   WsLibWrite (clptr->WsLibPtr, YouString, len, WSLIB_ASYNCH);

   /* greet and inform of (any) other participants */
   for (wsptr = NULL, cnt = 0; WsLibNext(&wsptr); cnt++);
   zptr = (sptr = Greeting) + sizeof(Greeting)-256;
   sptr += sprintf (sptr,
"\n<span style=\"font-size:0.8em;\">WASD</span>@%s\n%s;&nbsp; \
%d other participant%s%s",
                    ServerName, SOFTWAREID, cnt-1,
                    cnt-1 == 1 ? "" : "s", cnt-1 ? ";&nbsp; " : ".");
   for (wsptr = NULL; WsLibNext(&wsptr);)
      if ((cl2ptr = WsLibGetUserData(wsptr)) != clptr)
         sptr += sprintf (sptr, "%s%s@%s", cnt++ > 3 ? ", " : "",
                          cl2ptr->Handle, cl2ptr->RemoteHost);
   *sptr++ = '\n';
   if (sptr > zptr) EXIT_FI_LI(SS$_BUGCHECK);
   WsLibWrite (clptr->WsLibPtr, Greeting, sptr-Greeting, WSLIB_ASYNCH);

   /* queue an asynchronous read from the client */
   WsLibRead (clptr->WsLibPtr,
              clptr->InputBuffer,
              sizeof(clptr->InputBuffer),
              UpdateClient);

   ConnectedCount++;
}

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

void RemoveClient (struct WsLibStruct *wsptr)

{
   struct ChatClient  *clptr;

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

   clptr = WsLibGetUserData (wsptr);
   clptr->WsLibPtr = NULL;

   /* advise other clients of the disconnection */
   EchoChat (clptr);

   free (clptr);

   if (ConnectedCount) ConnectedCount--;
}

/*****************************************************************************/
/*
Asynchronous read from a WebSocket client has concluded.
Echo the chat to all connected clients.
*/

void UpdateClient (struct WsLibStruct *wsptr)

{
   char  *cptr, *sptr, *zptr;
   struct ChatClient  *clptr;

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

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

   clptr = WsLibGetUserData(wsptr);

   clptr->UpdateCount++;
   clptr->UnusedTime = CurrentTime + UnusedTimeout;

   if (!clptr->Handle[0])
   {
      /* first message is always the handle */
      zptr = (sptr = clptr->Handle) + sizeof(clptr->Handle)-1;
      for (cptr = clptr->InputBuffer; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      HtmlEscape (clptr->Handle, sizeof(clptr->Handle));
      clptr->InputBuffer[0] = '\0';
   }

   EchoChat (clptr);

   /* queue the next asynchronous read from the client */
   WsLibRead (clptr->WsLibPtr,
              clptr->InputBuffer,
              sizeof(clptr->InputBuffer),
              UpdateClient);
}

/*****************************************************************************/
/*
Build a WebSocket buffer with the details of the chat.  The buffer contains
four newline-delimited data that are split() by the JavaScript client and
displayed.  Write this buffer to all connected clients simultaneously updating
all with the new chat.
*/

void EchoChat (struct ChatClient *clptr)

{
   int  cnt, len, status;
   struct WsLibStruct  *wsptr;
   struct ChatClient  *cltptr;

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

   if (clptr->WsLibPtr)
   {
      if (*(USHORTPTR)clptr->InputBuffer == '?\0')
      {
         cnt = 0;
         for (wsptr = NULL; WsLibNext(&wsptr);) cnt++;
         len = sprintf (clptr->ChatBuffer, "%u\n%d participant%s\n",
                        clptr->YouIdent, cnt, cnt == 1 ? "" : "s");
         cnt = 0;
         for (wsptr = NULL; WsLibNext(&wsptr);)
         {
            if (len > sizeof(clptr->ChatBuffer)-256)
            {
               len += sprintf (clptr->ChatBuffer+len, "[overflow]");
               break;
            }
            else
            {
               cltptr = WsLibGetUserData(wsptr);
               if (cnt++) len += sprintf (clptr->ChatBuffer+len, ", ");
               len += sprintf (clptr->ChatBuffer+len, "%s@%s",
                               cltptr->Handle, cltptr->RemoteHost);
            }
         }
         len += sprintf (clptr->ChatBuffer+len, "\n");
         WsLibWrite (clptr->WsLibPtr, clptr->ChatBuffer, len, WSLIB_ASYNCH);
         return;
      }

      if (clptr->InputBuffer[0])
      {
         HtmlEscape (clptr->InputBuffer, sizeof(clptr->InputBuffer));
         len = sprintf (clptr->ChatBuffer, "%u\n%s@%s\n%s",
                        clptr->YouIdent, clptr->Handle,
                        clptr->RemoteHost, clptr->InputBuffer);
      }
      else
         len = sprintf (clptr->ChatBuffer, "%u\n%s@%s\n<i>[joining]</i>",
                        clptr->YouIdent, clptr->Handle,
                        clptr->RemoteHost);
   }
   else
      len = sprintf (clptr->ChatBuffer, "%u\n%s@%s\n<i>[disconnected]</i>",
                     clptr->YouIdent, clptr->Handle,
                     clptr->RemoteHost);

   for (wsptr = NULL; WsLibNext(&wsptr);)
      WsLibWrite (wsptr, clptr->ChatBuffer, len, 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 HtmlEscape
(
char *String,
int SizeOfString
)
{
   int  extralen;
   char  *cptr, *sptr, *zptr;

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

   extralen = 0;
   for (cptr = String; *cptr; cptr++)
   {
      switch (*cptr)
      {
         case '<' : extralen += 3; break;
         case '>' : extralen += 3; break;
         case '&' : extralen += 4; break;
      }
   }
   if (!extralen) return (cptr - String);

   zptr = String + SizeOfString - 1;
   sptr = cptr + extralen;
   if (sptr < zptr)
      *(unsigned short*)sptr = '\0\0';
   else
      *zptr = '\0';  
   while (cptr >= String)
   {
      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++;
   return (sptr - String);
}

/*****************************************************************************/
/*
Get the remote host name/address (blocking, IPv4 only).
*/

void RemoteHostName (struct ChatClient *clptr)

{
   int  IpAddr;
   struct hostent  *HostEntryPtr;

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

   /* if the server has host name resolution enabled */
   strcpy (clptr->RemoteHost, WsLibCgiVar("REMOTE_HOST"));
   /* otherwise resolve it ourselves */
   if (!strcmp (clptr->RemoteHost, WsLibCgiVar("REMOTE_ADDR")))
      if (!getenv ("WS_CHAT_NO_LOOKUP"))
         if ((IpAddr = inet_addr (clptr->RemoteHost)) != -1)
            if (HostEntryPtr = gethostbyaddr (&IpAddr, 4, AF_INET))
               strcpy (clptr->RemoteHost, HostEntryPtr->h_name);
}

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

