/*****************************************************************************/
/*
                                 autobahn.c

Autobahn test-bench client:

  http://autobahn.ws/testsuite

A sophisticated, comprehensive and indepdendent (non-WASD) test suite powered
by Python that validates WebSocket behaviour, particularly in corner cases. 
Used to exercise and validate the wsLIB.c library.  Many thanks to the author,
Tobias Oberstein for making this generally available.  It has been invaluable
in verifying and improving wsLIB behaviours.

This description is not a tutorial on the use of Autobahn, just how it was used
to validate the WASD WebSocket environment.  For the (considerable)
ins-and-outs of Autobahn see its own documentation.  Autobahn has a Twisted
package dependency which currently is not supported on VMS.

For WASD development purposes the Python platform used was OS-X 10.7.5

The comments below are based on Autobahn v0.5.2 and my test-bench environment.


INSTALL
-------
Based on instructions at:

  http://autobahn.ws/testsuite/installation

Installed on OS-X 10.7.5:

  git clone https://github.com/tavendo/AutobahnTestSuite.git
  cd ./AutobahnTestSuite/autobahntestsuite
  # alter timeouts for my test-bench
  perl -e "s/self.WAITSECS = 10/self.WAITSECS = 100/g;" -pi $(find ./autobahn/case/ -type f)
  sudo python setup.py install
  cd ./autobahntestsuite
  python wstest.py --help

Then the server test-bench executed using:

  cd ~/AutobahnTestSuite/autobahntestsuite/autobahntestsuite
  # edit "servers" changes described below
  nano fuzzingclient.json
  wstest -m fuzzingclient -s fuzzingclient.json


WSLIB AS CLIENT CODE
--------------------
The wsLIB code can be tested using the Autobahn "fuzzing server" on the same
or separate host using it as a command-line utility.

  $ AUTOBAHN == "$WASD_EXE:AUTOBAHN"

To automatically run all of the Autobahn test suite

  $ AUTOBAHN

To use where the Autobahn test server is located on another host

  $ AUTOBAHN the.other.host

Any of the specific Autobahn tests can be run by providing the URL

  $ AUTOBAHN "ws://localhost:9000/the_test_uri"

/CASE=<integer> runs the specified Autobahn test case.

/FRAME=<integer> sets the maximum frame size (default is none).

/MRS=<integer> allows the maximum record size of the socket to be specified
controlling the maximum fragment size generated by wsLIB (default is 65535).

/UPDATE the Autobahn "clients" report page.


MODIFYING TIMEOUTS
------------------
The default timeouts for Autobahn tests are a bit conservative for my test
bench setup and have required some tweaking.  For a 2^20 x 1 octet test (yes, a
million network datagrams each containing one octet!) and 100Mbps LAN and VMS
$QIO this takes *much* longer.  The timeout can be extended by modifying the
test Python code.  The following Perl command-lines did it for this author (at
the OS X command-line).

  perl -e "s/self.WAITSECS = 10/self.WAITSECS = 100/g;" -pi $(find ~/Autobahn/lib/python/autobahn/case/ -type f)

Ensure you change the timeout value before the

  cd ~/Autobahn/lib/python
  python setup.py install

or remove all of the *.pyc and reinstall after doing so.


WSLIB AS SERVER CODE
--------------------
The wsLIB code can also be tested as a WebSocket application (server script) by
placing it in the scripting directory and then using the "fuzzing test driver". 
The host and port may be specified other than localhost:9000 using the
fuzzing_client_spec.json file.  The following allowed the Autobahn client to be
on a separate system to the WASD server and WebSocket test application running
on 192.168.1.3.  The agent should be adjusted to reflect accurately the
environment.

  {
     "servers": [{ "agent":"WASDv10.2.0+1.0.4",
                   "url":"ws://192.168.1.3:9000",
                   "options": { "version": 17} }],
     "cases": ["*"]
  }

Retaining port 9000 is only for convenience.  The WASD server has a temporary
service configured at that port with mapping to activate the test script in
CGIplus mode for any WebSocket request on that service.

  # WASD_CONFIG_MAP
  if (websocket:) script+ /* /cgi-bin/autobahn/* map=once


COPYRIGHT
---------
Copyright (C) 2011-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 (update SOFTWAREVN as well!)
---------------
23-SEP-2012  MGD  v1.1.0, modify instructions for AutoBahn test suite v0.5.2
                          (no actual changes to code required)
13-AUG-2011  MGD  v1.0.0, initial
*/
/*****************************************************************************/

#define SOFTWAREVN "1.1.0"
#define SOFTWARENM "AUTOBAHN"
#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

#define COPYRIGHT_DATES "2020"

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

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>

/* VMS related header files */
#include <descrip.h>
#include <iodef.h>
#include <iosbdef.h>
#include <libclidef.h>
#include <libdef.h>
#include <lib$routines.h>
#include <ssdef.h>
#include <stsdef.h>
#include <starlet.h>

/* wsLIB header file */
#include "wslib.h"

#define BOOL int
#define TRUE 1
#define FALSE 0
 
#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define EXIT_LINE(status) { printf ("[AUTOBAHN:%d]", __LINE__); exit(status); }

struct ConnectionStruct
{
   int  ReadBufferCount,
        ReadBufferSize,
        WriteBufferCount,
        WriteBufferSize;

   char  *ReadBufferPtr,
         *WriteBufferPtr;

   struct WsLibStruct  *WsLibPtr;
};

char  Utility [] = "AUTOBAHN";

BOOL  CliUpdateReports,
      Debug,
      DoShowVersion;

int  CliCaseNumber,
     CliFrameMax,
     CliSocketMrs,
     ServerPort;

char  *RequestUriPtr;

char  CloseAdieu [128],
      CommandLine [256],
      ServerHost [256],
      RequestUri [256];


/* function prototypes */
void ClientRead (struct WsLibStruct*);
void ClientReadAst (struct WsLibStruct*);
void CloseConnection (struct WsLibStruct*);
void ConnectToServer (char*, int);
void EchoScriptReadAst (struct WsLibStruct*);
void GetParameters (int, char**);
void LaunchEchoScript ();
void MessageCallback (struct WsLibStruct*);
void ParseURL ();
int strsame (char*, char*, int);

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

int main
(
int argc,
char *argv[]
)
{
   int  cnt, status,
        CaseCount;
   char  *cptr, *sptr, *zptr;
   char  AgentWasd [64],
         CaseBuffer [64];

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

   if (getenv("AUTOBAHN$DBUG")) Debug = TRUE;

   GetParameters (argc, argv);

   if (DoShowVersion)
   {
      fprintf (stdout, "%%%s-I-VERSION, %s\n%s",
               Utility, SOFTWAREID, COPYRIGHT_FULL);
      exit (SS$_NORMAL);
   }

   sprintf (CloseAdieu, "%s bids adieu!", SOFTWAREID);

   sprintf (AgentWasd, "WASD-wsLIB-%s", strchr(WsLibVersion(),' ')+1);

   if (getenv ("HTTP$INPUT"))
   {
      /*****************/
      /* server script */
      /*****************/

      do {

         /* wait for next request (if CGIplus) */
         WsLibCgiVar ("");

         /* kick-off the echo listener */
         LaunchEchoScript ();

         /* end of request (if CGIplus) */
         WsLibCgiPlusEof ();

      } while (WsLibIsCgiPlus());

      exit (SS$_NORMAL);
   }

   /**************/
   /* CLI client */
   /**************/

   if (!RequestUriPtr) RequestUriPtr = "ws://localhost:9000/";

   ParseURL ();

   if (CliCaseNumber)
   {
      /* send the case request to Autobahn server */
      sprintf (RequestUri, "/runCase?case=%d&agent=%s",
               CliCaseNumber, AgentWasd);
      ConnectToServer (NULL, 0);
      sys$hiber ();
   }
   else
   if (CliUpdateReports)
   {
      /* send the update reports request to Autobahn server */
      sprintf (RequestUri, "/updateReports?agent=%s", AgentWasd);
      ConnectToServer (NULL, 0);
      sys$hiber ();
   }
   else
   if (!RequestUri[0])
   {
      /* run through full Autobahn test suite */
      strcpy (RequestUri, "/getCaseCount");
      ConnectToServer (CaseBuffer, sizeof(CaseBuffer));
      sys$hiber ();
      CaseCount = atoi(CaseBuffer);
      if (Debug) fprintf (stdout, "CaseCount: %d\n", CaseCount);

      for (cnt = 1; cnt <= CaseCount; cnt++)
      {
         fprintf (stdout, "case: %d\n", cnt);
         sprintf (RequestUri, "/runCase?case=%d&agent=%s", cnt, AgentWasd);
         ConnectToServer (NULL, 0);
         sys$hiber ();
         sleep (1);
      }

      sprintf (RequestUri, "/updateReports?agent=%s", AgentWasd);
      ConnectToServer (NULL, 0);
      sys$hiber ();
   }
   else
   {
      /* send the CLI request to Autobahn server */
      ConnectToServer (NULL, 0);
      sys$hiber ();
   }

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Connect to the server asynchronously.
*/
 
void ConnectToServer
(
char *ReadBuffer,
int BufferSize
)
{
   int  status;
   struct ConnectionStruct  *cxptr;

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

   if (Debug) fprintf (stdout, "ConnectToServer()\n");

   /* init connection structure */
   cxptr = calloc (1, sizeof(struct ConnectionStruct));
   if (!cxptr) EXIT_LINE (vaxc$errno);

   cxptr->WriteBufferSize = 1024;
   cxptr->WriteBufferPtr = calloc (1, cxptr->WriteBufferSize);
   if (!cxptr->WriteBufferPtr) EXIT_LINE (vaxc$errno);

   if (BufferSize)
   {
      /* use this supplied buffer */
      cxptr->ReadBufferSize = BufferSize;
      cxptr->ReadBufferPtr = ReadBuffer;
   }
   else
   {
      /* use wsLIB dynamic buffers */
      cxptr->ReadBufferSize = 0;
      cxptr->ReadBufferPtr = NULL;
   }

   cxptr->WsLibPtr = WsLibCreate (cxptr, &CloseConnection);

   if (CliSocketMrs) WsLibClSetSocketMrs (cxptr->WsLibPtr, CliSocketMrs);

   if (CliFrameMax) WsLibSetFrameMax (cxptr->WsLibPtr, CliFrameMax);

   if (Debug) cxptr->WsLibPtr->WatchScript = 1;

   WsLibSetRoleClient (cxptr->WsLibPtr);
   WsLibSetMsgCallback (cxptr->WsLibPtr, &MessageCallback);

   status = WsLibClConnect (cxptr->WsLibPtr,
                            ServerHost, ServerPort, RequestUri,
                            &ClientRead);
   if (Debug) fprintf (stdout, "WsLibClConnect() %%X%08.08X\n", status);
}

/****************************************************************************/
/*
Request is concluded.
*/

void CloseConnection (struct WsLibStruct *wsptr)

{
   int  status;
   struct ConnectionStruct  *cxptr;

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

   if (Debug) fprintf (stdout, "CloseConnection()\n");

   cxptr = WsLibGetUserData (wsptr);

   WsLibDestroy (cxptr->WsLibPtr);

   if (cxptr->WriteBufferPtr) free (cxptr->WriteBufferPtr);
   if (cxptr->ReadBufferPtr) free (cxptr->ReadBufferPtr);

   sys$wake (0, 0);

   free (cxptr);
}

/****************************************************************************/
/*
CLI client read asynchronously from Autobahn test server.
*/

void ClientRead (struct WsLibStruct *wsptr)

{
   int  status;
   struct ConnectionStruct  *cxptr;

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

   cxptr = WsLibGetUserData (wsptr);

   if (Debug) fprintf (stdout, "ClientRead()\n");

   if (VMSnok (status = WsLibClSocketStatus (wsptr)))
   {
      fprintf (stdout, "%%%s-W-CONNECT, [%d] failed (%%X%08.08X)\n",
                  Utility, __LINE__, status);
      sys$wake (0, 0);
      return;
   }

   /* read next frame */
   WsLibRead (cxptr->WsLibPtr,
              cxptr->ReadBufferPtr,
              cxptr->ReadBufferSize,
              ClientReadAst);
}

/****************************************************************************/
/*
CLI client read from Autobahn test server has completed.  Echoes the message
back to Autobahn test server.
*/

void ClientReadAst (struct WsLibStruct *wsptr)

{
   int  status,
        DataCount;
   char  *DataPtr;
   struct ConnectionStruct  *cxptr;

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

   if (Debug) fprintf (stdout, "ClientReadAst()\n");

   cxptr = WsLibGetUserData (wsptr);

   if (VMSnok (status = WsLibReadStatus(wsptr)))
   {
      /* WEBSOCKET_INPUT read error (can be EOF) */
      if (Debug) fprintf (stdout, "%%X%08.08X\n", status);
      return;
   }

   DataPtr = WsLibReadData(wsptr);
   DataCount = WsLibReadCount(wsptr);

   if (WsLibReadIsText (wsptr))
   {
      fprintf (stdout, "text: %d bytes\n", DataCount); 
      WsLibSetUtf8 (wsptr);
      WsLibWrite (wsptr, DataPtr, DataCount, NULL);
   }
   else
   if (WsLibReadIsBinary (wsptr))
   {
      fprintf (stdout, "binary: %d bytes\n", DataCount); 
      WsLibSetBinary (wsptr);
      WsLibWrite (wsptr, DataPtr, DataCount, NULL);
   }
   else
   {
      WsLibClose (wsptr, 0, NULL);
      return;
   }

   ClientRead (wsptr);
}

/*****************************************************************************/
/*
Create a connection structure and begin to read from the Autobahn test client.
It's all asynchronous from there.
*/
 
void LaunchEchoScript ()

{
   int  status;
   struct ConnectionStruct  *cxptr;
   struct WsLibStruct  *wsptr;

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

   if (Debug) fprintf (stdout, "LauchEcho()\n");

   /* init connection structure */
   cxptr = calloc (1, sizeof(struct ConnectionStruct));
   if (!cxptr) EXIT_LINE (vaxc$errno);

   cxptr->WsLibPtr = wsptr = WsLibCreate (cxptr, &CloseConnection);

   /* open the IPC to the WebSocket (mailboxes) */
   status = WsLibOpen (wsptr);
   if (VMSnok (status)) exit (status);

   /* read wait is ten seconds */
   WsLibSetReadSecs (wsptr, 10);

   /* no I/O interaction is 120 seconds */
   WsLibSetIdleSecs (wsptr, 120);

   /* set so that text frames are not automatially converted from UTF-8 */
   WsLibSetBinary (wsptr);

   /* read next frame (dynamically allocated buffer and no limit on size) */
   WsLibRead (wsptr, NULL, 0, EchoScriptReadAst);
}

/****************************************************************************/
/*
Read from Autobahn test client has completed, simply echo it back.
*/

void EchoScriptReadAst (struct WsLibStruct *wsptr)

{
   int  DataCount,
        UniCount;
   char  *DataPtr,
         *UniPtr;

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

   if (Debug) fprintf (stdout, "EchoScriptReadAst()\n");

   if (VMSnok (WsLibReadStatus(wsptr))) return;

   DataPtr = WsLibReadData(wsptr);
   DataCount = WsLibReadCount(wsptr);

   if (WsLibReadIsText (wsptr))
   {
      WsLibSetUtf8 (wsptr);
      WsLibWrite (wsptr, DataPtr, DataCount, NULL);
   }
   else
   if (WsLibReadIsBinary (wsptr))
   {
      WsLibSetBinary (wsptr);
      WsLibWrite (wsptr, DataPtr, DataCount, NULL);
   }
   else
   {
      WsLibClose (wsptr, 0, NULL);
      return;
   }

   /* read next frame (dynamically allocated buffer and no limit on size) */
   WsLibRead (wsptr, NULL, 0, EchoScriptReadAst);
}

/*****************************************************************************/
/*
Parse the CLI specified URL into it's host-name, port and URI.
*/
 
void ParseURL ()

{
   int  status;
   char  *cptr, *sptr;

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

   if (Debug) fprintf (stdout, "ParseURL() |%s|\n", RequestUriPtr);

   cptr = RequestUriPtr;
   if (strsame (cptr, "ws://", 5)) cptr += 5;
   sptr = ServerHost;
   while (*cptr && *cptr != ':' && *cptr != '/') *sptr++ = *cptr++;
   *sptr = '\0';
   if (*cptr == ':')
   {
      cptr++;
      if (isdigit(*cptr)) ServerPort = atoi(cptr);
      while (*cptr && isdigit(*cptr)) cptr++;
   }

   if (!ServerHost[0])
   {
      /* assume the local host is the server */
      gethostname (ServerHost, sizeof(ServerHost));
   }

   sptr = RequestUri;
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (!ServerPort) ServerPort = 9000;

   if (Debug)
      fprintf (stdout, "|%s|%d|%s|\n", ServerHost, ServerPort, RequestUri);
}
 
/****************************************************************************/
/*
Display non-informational messages output by the wsLIB library.
*/

void MessageCallback (struct WsLibStruct *wsptr)

{
   char  *cptr;
   struct ConnectionStruct  *cxptr;

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

   if (Debug) fprintf (stdout, "MessageCallback()\n");

   cxptr = WsLibGetUserData (wsptr);

   cptr = WsLibMsgString (wsptr);

   if (*cptr == 'I') return;

   fprintf (stdout,
"%%%s-W-WSLIB, error callback (WSLIB line %d)\n \\%s\\\n",
            Utility,  WsLibMsgLineNumber(wsptr), cptr);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  This function has
been customized for this utility to allow it to be called multiple times during
the one use.
*/

void GetParameters
(
int argc,
char *argv[]
)
{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  acnt, status;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

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

   if (Debug) fprintf (stdout, "GetParameters() %d\n", argc);

   /**************/
   /* initialize */
   /**************/

   if (argc <= 1) return;

   /* get the entire command line following the verb */
   if (VMSnok (status =
       lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
      exit (status);
   (clptr = CommandLine)[Length] = '\0';
   if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      /**************/
      /* parameters */
      /**************/

      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = TRUE;
         continue;
      }
      if (strsame (aptr, "/CASE=", 5))
      {
         while (*aptr && !isdigit(*aptr)) aptr++;
         CliCaseNumber = atoi(aptr);
         continue;
      }
      if (strsame (aptr, "/FRAME=", 5))
      {
         while (*aptr && !isdigit(*aptr)) aptr++;
         CliFrameMax = atoi(aptr);
         continue;
      }
      if (strsame (aptr, "/MRS=", 5))
      {
         while (*aptr && !isdigit(*aptr)) aptr++;
         CliSocketMrs = atoi(aptr);
         continue;
      }
      if (strsame (aptr, "/UPDATE", -1))
      {
         CliUpdateReports = TRUE;
         continue;
      }
      if (strsame (aptr, "/VERSION", 4))
      {
         DoShowVersion = TRUE;
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!RequestUriPtr)
      {
         RequestUriPtr = aptr;
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
TRUE if two strings are the same, or FALSE if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 
 
BOOL strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (FALSE);
      if (count)
         if (!--count) return (TRUE);
   }
   if (*sptr1 || *sptr2)
      return (FALSE);
   else
      return (TRUE);
}
 
/****************************************************************************/

