/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                                  raw_echo.c

A demonstrator for CGIplus RawSocket scripts.  Also see rawLIB.c source.

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

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

(And any resemblence to any WS_ECHO.C living or dead is purely coincidental :-)

Example configuration:

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

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

Then to test:

  telnet server-host-name 1234

  ^@ will switch between character and line modes
  ^T will print connected client data
  ^D and ^Z result in application disconnect

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


COPYRIGHT
---------
Copyright (C) 2016-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
---------------
04-DEC-2016  MGD  v1.0.0, initial development
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#define SOFTWAREVN "1.0.0"
#define SOFTWARENM "RAW_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 "rawlib.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 "RAW_ECHO", __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 */

int  ConnectedCount,
     UnusedTimeout = DEFAULT_UNUSED_TIMEOUT,
     UsageCount;

struct EchoClient {

   int  CharCount,
        CloseFrame,
        LineCount,
        LineMode,
        PrevCharCount,
        TelnetEcho,
        TelnetBinary;

   unsigned int  ConnectTime,
                 UnusedTime;

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

   struct RawLibStruct  *RawLibPtr;

   struct dsc$descriptor_s  InputDsc,
                            StatusDsc;
};

/* function prototypes */
void AddClient ();
void EchoThis (struct RawLibStruct*);
void NextRequest ();
void OutputClientStatus (struct RawLibStruct*);
void OutputLine (struct RawLibStruct*);
void RemoveClient (struct RawLibStruct*);
uchar* TelnetCommand (struct RawLibStruct*, uchar*, int);
void TelnetLineMode (struct RawLibStruct*);

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

main ()

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

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

   RawLibInit ();

   /* 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("RAW_ECHO_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)

{
   static $DESCRIPTOR (GreetingFaoDsc, "Hello !AZ !AZ (!AZ) at !AZ\r\n");

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

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

   CurrentTime = RawLibTime ();

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

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

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

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

   TelnetLineMode (clptr->RawLibPtr);

   clptr->StatusDsc.dsc$w_length = sizeof(clptr->StatusBuffer);
   sys$fao (&GreetingFaoDsc, &clptr->StatusDsc.dsc$w_length,
            &clptr->StatusDsc, clptr->RemoteHost,
            SOFTWAREID, RawLibVersion(), clptr->ServerName);
   RawLibWriteDsc (clptr->RawLibPtr, &clptr->StatusDsc, RAWLIB_ASYNCH);
   RawLibWrite (clptr->RawLibPtr, "[Char Mode]\r\n", 13, RAWLIB_ASYNCH);

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

   ConnectedCount++;
}

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

void RemoveClient (struct RawLibStruct *rawptr)

{
   struct EchoClient  *clptr;

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

   clptr = RawLibGetUserData (rawptr);

   free (clptr);

   if (ConnectedCount) ConnectedCount--;
}

/*****************************************************************************/
/*
Asynchronous read from a RawSocket client has concluded.
*/

void EchoThis (struct RawLibStruct *rawptr)

{
   int  len;
   uchar  *cptr, *czptr;
   struct EchoClient  *clptr;

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

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

   clptr = RawLibGetUserData(rawptr);

   clptr->UnusedTime = RawLibTime() + UnusedTimeout;

   /* CAUTION! with multiple concurrent write status becomes unreliable */
   cptr = (uchar*)clptr->InputDsc.dsc$a_pointer;
   czptr = cptr + clptr->InputDsc.dsc$w_length;

   while (cptr < czptr)
   {
      if (*cptr == TELNET_IAC)
      {
         /******************/
         /* telnet command */
         /******************/

         cptr = TelnetCommand (rawptr, cptr, czptr - cptr);
         if (cptr >= czptr) break;
         if (*cptr != TELNET_IAC) continue;
      }

      /* implicit converstion of linemode <CR><NUL> to <CR><LF> */
      if (*cptr == 0x0d && *(cptr+1) == 0x00) *(cptr+1) = '\n';

      if (*cptr == 0x00)
      {
         /**********************************/
         /* per-keystroke/line toggle (^@) */
         /**********************************/

         if (clptr->LineMode && clptr->CharCount) OutputLine (rawptr);
         clptr->CharCount = 0;
         if (clptr->LineMode = !clptr->LineMode)
            RawLibWrite (rawptr, "[Line Mode]\r\n", 13, NULL);
         else
            RawLibWrite (rawptr, "[Char Mode]\r\n", 13, NULL);
         cptr++;
      }
      else
      if (*cptr == 0x14)
      {
         /**********************/
         /* client status (^T) */
         /**********************/

         OutputClientStatus (rawptr);
         cptr++;
      }
      else
      if (*cptr == 0x04 || *cptr == 0x1a)
      {
         /*************************/
         /* disconnect (^D or ^Z) */
         /*************************/

         RawLibClose (rawptr);
         return;
      }
      else
      if (*cptr == 0x0d && clptr->LineMode)
      {
         /*********************/
         /* per-line and <CR> */
         /*********************/

         OutputLine (rawptr);
         /* step over the <CR><LF> */
         cptr++;
         if (cptr < czptr && *cptr == '\n') cptr++;
      }
      else
      if (clptr->LineMode)
      {
         /************/
         /* per line */
         /************/


         if (clptr->CharCount < sizeof(clptr->LineBuffer)-1)
            clptr->LineBuffer[clptr->CharCount++] = *cptr;
         cptr++;
      }
      else
      {
         /*****************/
         /* per keystroke */
         /*****************/

         /* asynchronous (without AST target) */
         RawLibWrite (clptr->RawLibPtr, (char*)cptr, 1, NULL);
         if (*cptr == '\n')
            clptr->CharCount = 0;
         else
            clptr->CharCount++;
         cptr++;
      }
   }

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

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

void OutputLine (struct RawLibStruct *rawptr)

{
   static $DESCRIPTOR (BufferDsc, "");
   static $DESCRIPTOR (LineFaoDsc, "!UL: !AS\r\n");

   ushort  slen;
   char  LineBuffer [32+256];
   $DESCRIPTOR (LineDsc, LineBuffer);
   struct EchoClient  *clptr;

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

   clptr = RawLibGetUserData (rawptr);

   BufferDsc.dsc$a_pointer = clptr->LineBuffer;
   BufferDsc.dsc$w_length = clptr->CharCount;
   clptr->CharCount = 0;
   sys$fao (&LineFaoDsc, &slen, &LineDsc,
            ++clptr->LineCount, &BufferDsc);
   LineDsc.dsc$w_length = slen;

   /* blocking (for the test-bench) */
   RawLibWriteDsc (rawptr, &LineDsc, NULL);
}

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

void OutputClientStatus (struct RawLibStruct *rawptr)

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

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

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

   CurrentTime = RawLibTime ();

   clptr = RawLibGetUserData (rawptr);

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

   sys$fao (&StatusFaoDsc,
            &clptr->StatusDsc.dsc$w_length,
            &clptr->StatusDsc,
            clptr->CharCount && clptr->CharCount > clptr->PrevCharCount ?
               "\r\n" : "",
            clptr->RemoteHost,
            CurrentTime - clptr->ConnectTime,
            clptr->UnusedTime - CurrentTime,
            ConnectedCount, UsageCount,
            clptr->LineMode ? "Line" : "Char");

  clptr->PrevCharCount = clptr->CharCount;

   RawLibWriteDsc (rawptr, &clptr->StatusDsc, RAWLIB_ASYNCH);
}

/*****************************************************************************/
/*
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 provides each character as entered (disable client line
buffer/edit) and the server will echo.
*/

void TelnetLineMode (struct RawLibStruct *rawptr)

{
#define OPT_ECHO 1
#define OPT_LINEMODE 34

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

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

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

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

