/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                                 raw_chat.c

(What must be a "classic") demonstrator for CGIplus RawSocket scripts.  It's
pretty simple-minded, and at the command-line allows lots of confusion when
another party's chat appears during local line editing.  However the purpose is
not to create a chat environment, only to demonstrate (and exercise) the
RawSocket environment.  Also see rawLIB.c source.

Suppress host name lookup by defining the logical name WS_CHAT_NO_LOOKUP.

Experiment with the RawLibOnNextRequest() approach to request synchronisation by
defining the logical name WS_CHAT_ON_NEXT_REQUEST.

Example configuration:

  # WASD_CONFIG_SERVICE
  [[http:*:1234]]
  [ServiceRawSocket]  enabled

  # WASD_CONFIG_MAP
  [[*:1234]]
  map * /cgiplus-bin/raw_chat

Then to test:

  telnet server-host-name 1234

As the telnet client is the obvious end-user application RAW_CHAT.C implements
a constrained set of telnet commands and associated options.


COPYRIGHT
---------
Copyright (C) 2016-2020 Mark G.Daniel
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY
---------------
10-SEP-2016  MGD  v1.0.0, initial development
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#define SOFTWAREVN "1.0.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 "rawlib.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 TELNET_SE   240  /* F0 */
#define TELNET_NOP  241  /* F1 */
#define TELNET_DM   242  /* F2 */
#define TELNET_BRK  243  /* F3 */
#define TELNET_IP   244  /* F4 */
#define TELNET_AO   245  /* F5 */
#define TELNET_AYT  246  /* F6 */
#define TELNET_EC   247  /* F7 */
#define TELNET_EL   248  /* F8 */
#define TELNET_GA   249  /* F9 */
#define TELNET_SB   250  /* FA */
#define TELNET_WILL 251  /* FB */
#define TELNET_WONT 252  /* FC */
#define TELNET_DO   253  /* FD */
#define TELNET_DONT 254  /* FE */
#define TELNET_IAC  255  /* FF */

#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  UpdateCount,
        YouIdent;

   unsigned int  UnusedTime;

   char  ChatBuffer [4096],
         ConnectTime [32],
         Handle [64],
         InputBuffer [1024],
         LineBuffer [1024],
         RemoteHost [128];

   struct RawLibStruct  *RawLibPtr;
};

/* function prototypes */
void AddClient ();
void EchoChat (struct ChatClient*);
void NextRequest ();
void RemoteHostName (struct ChatClient*);
void RemoveClient (struct RawLibStruct *rawptr);
void UpdateClient (struct RawLibStruct*);
uchar* TelnetCommand (struct RawLibStruct*, uchar*, int);
void TelnetLineMode (struct RawLibStruct*);

/*****************************************************************************/
/*
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 RawSocket 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 */
   RawLibSetLifeSecs (2*60);

   if (!RawLibIsCgiPlus())
   {
      fprintf (stdout, "Status: 500\r\n\r\n\
RawSocket needs to be CGIplus!\n");
   }
   else
   if (getenv("WS_CHAT_ON_NEXT_REQUEST") != NULL)
   {
      RawLibOnNextRequest (NextRequest);
      /* asynchronous from here-on-in */
      for (;;) sys$hiber();
   }
   else
   {
      for (;;)
      {
         RawLibCgiVar ("");

         sys$setast (0);

         UsageCount++;

         AddClient (0);

         RawLibCgiPlusEof ();

         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);

   RawLibCgiPlusEof ();
}

/*****************************************************************************/
/*
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 RawLibStruct  *rawptr;
   struct ChatClient  *clptr, *cl2ptr;

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

   if (!ServerName[0]) strcpy (ServerName, RawLibCgiVar ("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 RawSocket library structure for the client */
   if (!(clptr->RawLibPtr = rawptr = RawLibCreate (clptr, RemoveClient)))
   {
      /* failed, commonly on some RawSocket protocol issue */
      free (clptr);
      return;
   }

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

   /* open the IPC to the RawSocket (mailboxes) */
   status = RawLibOpen (clptr->RawLibPtr);
   if (VMSnok(status)) EXIT_FI_LI(status);

   TelnetLineMode (rawptr);

   /* if asynchronous next-request convey this to the JavaScript client */
   if (OnNext) RawLibWrite (clptr->RawLibPtr, "OnNext", 6, RAWLIB_ASYNCH);

   /* greet and inform of (any) other participants */
   for (rawptr = NULL, cnt = 0; RawLibNext(&rawptr); cnt++);
   zptr = (sptr = Greeting) + sizeof(Greeting)-256;
   sptr += sprintf (sptr,
"\r\n<-| WASD@%s\r\n\
<-| %s; %d other participant%s%s",
                    ServerName, SOFTWAREID, cnt-1,
                    cnt-1 == 1 ? "" : "s", cnt-1 ? "; " : ".");
   for (rawptr = NULL; RawLibNext(&rawptr);)
      if ((cl2ptr = RawLibGetUserData(rawptr)) != clptr)
         sptr += sprintf (sptr, "%s%s@%s", cnt++ > 3 ? ", " : "",
                          cl2ptr->Handle, cl2ptr->RemoteHost);
   sptr += sprintf (sptr, "\r\n<-| Please enter your nick-name...\r\n->| ");
   if (sptr > zptr) EXIT_FI_LI(SS$_BUGCHECK);
   RawLibWrite (clptr->RawLibPtr, Greeting, sptr-Greeting, RAWLIB_ASYNCH);

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

   ConnectedCount++;
}

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

void RemoveClient (struct RawLibStruct *rawptr)

{
   struct ChatClient  *clptr;

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

   clptr = RawLibGetUserData (rawptr);
   clptr->RawLibPtr = NULL;

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

   free (clptr);

   if (ConnectedCount) ConnectedCount--;
}

/*****************************************************************************/
/*
Echo the chat to all connected clients.
*/

void UpdateClient (struct RawLibStruct *rawptr)

{
   int  cnt;
   uchar  *cptr, *czptr, *sptr, *zptr;
   struct ChatClient  *clptr;

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

   if (VMSnok (RawLibReadStatus(rawptr)))
   {
      /* WEBSOCKET_INPUT read error (can be EOF) */
      RawLibClose (rawptr);
      return;
   }

   clptr = RawLibGetUserData(rawptr);

   cnt = RawLibReadCount(rawptr);
   czptr = (cptr = (uchar*)clptr->InputBuffer) + cnt;
   zptr = (sptr = (uchar*)clptr->LineBuffer) + sizeof(clptr->LineBuffer);
   while (cptr < czptr && sptr < zptr)
   {
      if (*cptr == TELNET_IAC)
      {
         cptr = TelnetCommand (rawptr, cptr, czptr-cptr);
         if (*cptr == TELNET_IAC) *sptr++ = *cptr++; 
      }
      else
         *sptr++ = *cptr++;
   }
   *sptr = '\0';

   if (clptr->LineBuffer[0])
   {
      if (!clptr->Handle[0])
      {
         /* first message should always be the handle */
         zptr = (sptr = (uchar*)clptr->Handle) + sizeof(clptr->Handle)-1;
         for (cptr = (uchar*)clptr->LineBuffer;
              *cptr && *cptr != '\r' && sptr < zptr;
              *sptr++ = *cptr++);
         *sptr = '\0';
         clptr->LineBuffer[0] = '\0';
      }
      EchoChat (clptr);
   }

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

/*****************************************************************************/
/*
Build a RawSocket 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  len;
   struct RawLibStruct  *rawptr;

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

   if (clptr->RawLibPtr)
   {
      if (clptr->LineBuffer[0])
         len = sprintf (clptr->ChatBuffer,
                        "\r\n<-| %u {%s@%s} %s->| ",
                        clptr->YouIdent, clptr->Handle,
                        clptr->RemoteHost, clptr->LineBuffer);
      else
         len = sprintf (clptr->ChatBuffer,
                        "\r\n<-| %u {%s@%s} [joining]\r\n->| ",
                        clptr->YouIdent, clptr->Handle,
                        clptr->RemoteHost);
   }
   else
      len = sprintf (clptr->ChatBuffer,
                     "\r\n<-| %u {%s@%s} [disconnected]\r\n->| ",
                     clptr->YouIdent, clptr->Handle,
                     clptr->RemoteHost);

   for (rawptr = NULL; RawLibNext(&rawptr);)
      RawLibWrite (rawptr, clptr->ChatBuffer, len, RAWLIB_ASYNCH);
}

/*****************************************************************************/
/*
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, RawLibCgiVar("REMOTE_HOST"));
   /* otherwise resolve it ourselves */
   if (!strcmp (clptr->RemoteHost, RawLibCgiVar("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);
}

/*****************************************************************************/
/*
Simple-minded example assumes the command and options are all in-buffer.  Just
ignore all that can be gotten away with.  Returns a pointer to the next
available character (if any).
*/

uchar* TelnetCommand
(
struct RawLibStruct *rawptr,
uchar *DataPtr,
int DataLength
)
{
   uchar  cmd;
   uchar  *cptr, *czptr;

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

   /* IAC is *always* followed by a command (so we always need two) */
   czptr = (cptr = DataPtr) + DataLength - 1;

   while (cptr < czptr && *cptr == TELNET_IAC)
   {
      cptr++;
      cmd = *cptr;
      if (cmd == TELNET_IAC) return (cptr);
      cptr++;
      if (cmd == TELNET_AYT)
      {
         RawLibWrite (rawptr, "\a", 1, RAWLIB_ASYNCH);
         continue;
      }
      if (cmd == TELNET_SB)
      {
         while (cptr < czptr)
         {
            if (*cptr == TELNET_IAC && *(cptr+1) == TELNET_SE)
            {
               cptr += 2;
               break;
            }
            cptr++;
         }
      }
      else
      {
         /* step over the option */
         if (cmd == TELNET_DO ||
             cmd == TELNET_DONT ||
             cmd == TELNET_WILL ||
             cmd == TELNET_WONT) cptr++;
      }
   }

   return (cptr);
}

/*****************************************************************************/
/*
Request the client enters per-line mode.
*/

void TelnetLineMode (struct RawLibStruct *rawptr)

{
#define OPT_ECHO 1
#define OPT_SGA  3

   static char DoCharBinary [] = { TELNET_IAC, TELNET_DO, OPT_ECHO,
                                   TELNET_IAC, TELNET_DO, OPT_SGA };

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

   RawLibWrite (rawptr, DoCharBinary, sizeof(DoCharBinary), RAWLIB_ASYNCH);
}

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

