/*****************************************************************************/
/*
                          authagent_example.c

Example authentication agent CGIplus script, using the callout mechanism.

For some in-built usernames and passwords that can be used for
experimentation/demonstration see AuthenticateUser() structure array
UserNamePasswordPairs[].

This example can be used in four ways.

1.  As a realm authenticator.

Here the agent is simply used to authenticate the user password for the realm.

  ["Authentication Agent test 1"=AUTHAGENT_EXAMPLE=agent]
  /some/path/or/other/*

Load the new authorization and access the path (for username/password details
see AuthenticateUser() below).  Of course processing can be WATCHed.

2.  For determining group membership.

  ["Authentication Agent test 2"=AUTHAGENT_EXAMPLE=agent;AUTHAGENT_EXAMPLE=agent]
  /some/path/or/other/*

Now group membership must be determined before access is granted (the agent is
used twice, first for authentication then for group membership).  Note that in
MemberOfGroup() only "moe", "larry" and "curly" are group members, "mark" is
not and so should be refused access.

3.  As a complete authenticator/authorizor.

Using the path, query string, remote host, etc., CGI variables, along with
authentication-specific ones, the agent can assume the role of complete
authenticator with any and all checks and processing desired.  To provide
per-path information to such an authenticator the "param=" authorization
keyword allows any parameter(s) required (they can be placed in single or
double quotes if required).  Typically this might be the source of the
authentication.  The third example demonstrates this.

  ["Authentication Agent test 3"=AUTHAGENT_EXAMPLE=agent]
  /some/path/or/other/* param=WASD_ROOT:[SRC.CGIPLUS]AUTHAGENT_EXAMPLE.LIS

4.  As a non-username/password authenticator

Normally an agent uses the WWW_REMOTE_USER and WWW_AUTH_PASSWORD, or the
WWW_HTTP_AUTHORIZATION, CGI variables to obtain authorization information
supplied in the request header.  These are initially requested by the server
automatically generating a 401/WWW-Authorize: response when there are none
present.  Of course it is possible for an agent to perform authorization
outside of this mechanism (examples implemented internally by the WASD server
include SSL X.509 client certificate, and the RFC1413 identification protocol). 
To suppress the automatic requesting of a username/password by the server make
the first portion of the agent parameter "/NO401".  The server detects this and
suppresses it's initial 401 response.  For example

  ["Authentication Agent test 4"=AUTHAGENT_EXAMPLE=agent]
  /some/path/or/other/* param="/NO401 ANY-OTHER-PARAMETER"


GENERAL GUIDELINES
------------------
The CGI variable WWW_AUTH_AGENT will *only* exist for authentication agents and
may be used to verify the HTTPd invoked the script, not a user.  If this
variable does not exist the authentication agent should immediately exit.  The
agent should also check that it is executing within the CGIplus environment and
also immediately exit if not.  All normal CGI/CGIplus variables are available
for use if desired (.e.g WWW_PATH_INFO, WWW_QUERY_STRING, etc.)

The server processes records, hence each "line" of output must be fflush()ed so
as not to be buffered by the CRTL.

The authentication agent may request the server to directly output an error
message.   The response line must begin "500 ", with any text following the
status code being output as the error message.  Generally this would not be
necessary as the server will generate an appropriate message for authentication
or group membership failure.

The WATCH facility is a valuable adjunct in understanding/debugging agent
script behaviour.  Response lines beginning "000 " are ignored and may be used
to provide WATCHable debug information (remembering all I/O adds to overhead in
production scripts).

(Debug) may also be turned on.  Although this will prevent the script engaging
in a dialog with the server it will output the debug information directly to
the browser, which may provide further information when developing/debugging.


AUTHENTICATING A USERNAME/PASSWORD
----------------------------------
The transaction details are found in the following CGI variables.

WWW_AUTH_AGENT .................. "REALM" or other parameter
WWW_AUTH_PASSWORD ............... user supplied case-sensitive password
WWW_AUTH_REALM .................. realm name (same as agent name)
WWW_AUTH_REALM_DESCRIPTION ...... realm description user is prompted with
WWW_REMOTE_USER ................. case-sensitive username

The WWW_AUTH_AGENT variable indicates whether authentication ("REALM") or group
membership ("GROUP") is being requested.  The realm description could be used
to differentiate multiple authentication realms to the one script (as
WWW_AUTH_REALM is always set to the name of the agent script).

Return a response (digits and 'access' are mandatory, other text is optional -
although can provide valuable development/debugging information):

'000 any text' ........... ignored by the server, provides WATCHable debug info 
'100 AUTHAGENT-CALLOUT' .. server abort if the agent is being run as a script
'100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite)
'100 NOCACHE' ............ do not cache the results of this authorization
'100 REASON any text' .... reason for the authentication being denied
'100 REMOTE-USER name .... provide user name (authenticated some non-401 way)
'100 VMS-USER name ....... this username is a VMS username, treat it as such
'100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header)
'100 USER any text' ...... provide user details (only after 200 response)
'200 access' ............. the username/password verified
                           access: "READ", "WRITE", "READ+WRITE", "FULL"
'401 reason' ............. username/password did not verify
'401 "realm"' ............ did not verify, (quoted) browser prompt string
'403 reason' ............. access is forbidden
'500 description' ........ script error to be reported via server

VMS-USER issues: when a VMS-USER is passed back to the server the username
undergoes all VMS authorization processing (e.g. ID, prime days, etc) - except
password checking, it is assumed the agent has authenticated the username.  The
access level (R, R+W, etc.) is derived from the SYSUAF information - unless the
agent *subsequently* provides a "200 access" callout.  The user details come
from the SYSUAF - unless the agent *subsequently* provides a "100 USER details"
callout.


ESTABLISHING GROUP MEMBERSHIP
-----------------------------
The transaction details are found in the following CGI variables.

WWW_AUTH_AGENT .................. "GROUP"
WWW_AUTH_GROUP .................. name of group
WWW_REMOTE_USER ................. case-sensitive username

Valid responses (digits are mandatory, other text is optional):

'000 any text' ........... ignored by the server, provides WATCHable debug info 
'100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite)
'100 NOCACHE' ............ do not cache the results of this authorization
'100 REASON any text' .... reason for the authentication being denied
'100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header)
'200 any text' ........... indicates group membership
'403 reason' ............. indicates not a group member
'500 description' ........ script error to be reported via server


LOGICAL NAMES
-------------
AUTHAGENT_EXAMPLE_NO401       see above
AUTHAGENT_EXAMPLE$DBUG        turns on all "if (Debug)" statements
AUTHAGENT_EXAMPLE$WATCH       turns on agent "000 message" WATCH statements

Debug statements do not work very well inside authentication agent scripts. 
Use the WATCH statements using the server WATCH facility to observe script
processing (for debug purposes).


BUILD DETAILS
-------------
Compile then link:
  $ @BUILD_AUTHAGENT_EXAMPLE
To just link:
  $ @BUILD_AUTHAGENT_EXAMPLE LINK


COPYRIGHT
---------
Copyright (C) 1999-2022 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)
---------------
10-JUN-2021  MGD  v2.0.0, minimal changes for WASD v12... agent requirements
11-MAY-2007  MGD  v1.6.2, belt-and-braces
23-DEC-2003  MGD  v1.6.1, minor conditional mods to support IA64
08-MAR-2003  MGD  v1.6.0, add '100 REASON text' for username/password failure
28-FEB-2001  MGD  v1.5.0, example of /NO401 functionality
01-FEB-2001  MGD  v1.4.0, add VMS-USER and demonstration capability
18-JAN-2001  MGD  v1.3.0, an agent can now '100 REMOTE-USER username'
06-DEC-2000  MGD  v1.2.0, an agent can now '100 SET-COOKIE rfc2109-cookie'
28-OCT-2000  MGD  v1.1.0, use CGILIB object module
06-SEP-1999  MGD  v1.0.0, initial development
*/

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

#define SOFTWAREVN "2.0.0"
#define SOFTWARENM "AUTHAGENT_EXAMPLE"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

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

/* VMS related header files */
#include <descrip.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header files */
#include "cgilib.h"

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

#define boolean int
#define true 1
#define false 0

/******************/
/* global storage */
/******************/

char  Utility [] = "AUTHAGENT_EXAMPLE";

int  CgiPlusUsageCount;

boolean  Debug,
         DebugWatch;

char  SoftwareId [64];

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

main ()
       
{
   int  status;
   char  *cptr;

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

   sprintf (SoftwareId, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   Debug = (getenv ("AUTHAGENT_EXAMPLE$DBUG") != NULL);
   DebugWatch = (getenv ("AUTHAGENT_EXAMPLE$WATCH") != NULL);
   CgiLibEnvironmentSetDebug (Debug);

   CgiLibEnvironmentInit (0, NULL, false);

   /* MUST only be executed in a CGIplus environment! */
   if (!CgiLibEnvironmentIsCgiPlus ())
   {
      CgiLibResponseHeader (502, "text/plain");
      fputs ("CGIplus!\n", stdout);
      exit (SS$_NORMAL);
   }

   for (;;)
   {
      /* block waiting for the next request */
      CgiLibVar ("");
      CgiPlusUsageCount++;

      if (cptr = CgiLibVarNull("REQUEST_METHOD"))
         if (!*cptr)
         {
            /* proctored into existance */
            CgiLibResponseHeader (204, "application/proctor");
            CgiLibCgiPlusEOF ();
            continue;
         }

      CgiLibCgiPlusCallout ("!AGENT-BEGIN: %s (%s) usage:%d",
                            SoftwareId, CgiLibEnvironmentVersion(),
                            CgiPlusUsageCount);

      ProcessRequest ();

      CgiLibCgiPlusCallout ("!AGENT-END:");

      CgiLibCgiPlusEOF ();
   }
}

/*****************************************************************************/
/*
Main authentication request processing function.
*/

ProcessRequest ()
       
{
   int  status;
   char  *AllowedClientPtr,
         *AuthAgentPtr,
         *PasswordPtr,
         *RemoteAddrPtr,
         *UserNamePtr;

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

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

   /* provide the server attention "escape" sequence record */
   if (!Debug) CgiLibCgiPlusESC ();

   /* ensure this is being invoked by the server */
   if ((AuthAgentPtr = CgiLibVarNull ("WWW_AUTH_AGENT")) == NULL)
      exit (SS$_ABORT);

   /* belt and braces */
   fprintf (stdout, "100 AUTHAGENT-CALLOUT\n");
   fflush (stdout);

   if (DebugWatch)
   {
      /* just a comment that can be WATCHed */
      fprintf (stdout, "000 [%d] %s\n", __LINE__, SoftwareId);
      fflush (stdout);
   }

   if (strsame (AuthAgentPtr, "REALM", -1))
   {
      /* "standard" use of agent to authenticate user */
      UserNamePtr = CgiLibVar ("WWW_REMOTE_USER");
      PasswordPtr = CgiLibVar ("WWW_AUTH_PASSWORD");
      AuthenticateUser (UserNamePtr, PasswordPtr);
   }
   else
   if (strsame (AuthAgentPtr, "GROUP", -1))
   {
      /* "standard" use of agent to authorize group membership */
      UserNamePtr = CgiLibVar ("WWW_REMOTE_USER");
      MemberOfGroup (UserNamePtr);
   }
   else
   if (strsame (AuthAgentPtr, "/NO401", 6))
   {
      /* non-user/pass authorization (example based on client IP address) */
      AllowedClientPtr = getenv ("AUTHAGENT_EXAMPLE_NO401");
      RemoteAddrPtr = CgiLibVar ("WWW_REMOTE_ADDR");
      if (AllowedClientPtr == NULL ||
          !strsame (AllowedClientPtr, RemoteAddrPtr, -1))
      {
         /* logical name not defined or not the same as client IP address */
         fprintf (stdout, "403 Not %s\n",
                  AllowedClientPtr == NULL ? "defined!" : AllowedClientPtr);
         fflush (stdout);
      }
      else
      {
         /* logical name is the same as client IP address */
         fprintf (stdout, "200 READ+WRITE\n");
         fflush (stdout);
      }
   }
   else
   {
      /* HTTPD$AUTH "param=" used to pass authentication source file name */
      UserNamePtr = CgiLibVar ("WWW_REMOTE_USER");
      PasswordPtr = CgiLibVar ("WWW_AUTH_PASSWORD");
      AuthenticateFromList (AuthAgentPtr, UserNamePtr, PasswordPtr);
   }

   /* provide the "escape" end-of-text sequence record */
   if (!Debug) CgiLibCgiPlusEOT ();
}

/*****************************************************************************/
/*
Example username/password verification function.
*/

AuthenticateUser
(
char *UserNamePtr,
char *PasswordPtr
)
{
   static struct
   {
      char *UserName;
      char *Password;
      char *Details;
      boolean ReturnCookie;
      boolean VmsUser;
   } UserNamePasswordPairs [] =
   {
      { "username", "password", "(just to show which is which)", false, false },
      { "mark", "daniel", "Mark Daniel, +61 8 82596031", false, false },
      { "daniel", "daniel", "Mark Daniel (VMS-USER)", false, true },
      { "no-such-user", "no-such-user", "fails (VMS-USER)", false, true },
      { "moe", "howard", "Moses Horwitz", true, false },
      { "larry", "fine", "Larry Fine", true, false },
      { "curly", "howard", "Jakob Horwitz", true, false },
      { NULL, NULL }
   };

   int  idx;
   char  *cptr;

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

   if (Debug)
      fprintf (stdout, "AuthenticateUser() |%s|%s|\n",
               UserNamePtr, PasswordPtr);

   if (DebugWatch)
   {
      /* just a comment that can be WATCHed */
      fprintf (stdout, "000 [%d] authenticating \"%s\"\n",
               __LINE__, UserNamePtr);
      fflush (stdout);
   }

   cptr = NULL;
   for (idx = 0; UserNamePasswordPairs[idx].UserName != NULL; idx++)
   {
      if (!strsame (UserNamePtr, UserNamePasswordPairs[idx].UserName, -1))
         continue;
      cptr = "Password validation failure";
      if (!strsame (PasswordPtr, UserNamePasswordPairs[idx].Password, -1))
         continue;
      cptr = NULL;
      /* both the string and the status code are valid here */
      fprintf (stdout, "200 READ+WRITE\n");
      fflush (stdout);
      /* supply an informational - the user's details */
      fprintf (stdout, "100 USER %s\n", UserNamePasswordPairs[idx].Details);
      fflush (stdout);

      if (UserNamePasswordPairs[idx].VmsUser)
      {
         /* force this to be a VMS user (just for demonstration purposes) */
         fprintf (stdout, "100 VMS-USER %s\n",
                  UserNamePasswordPairs[idx].UserName);
         fflush (stdout);
      }

      if (UserNamePasswordPairs[idx].ReturnCookie)
      {
         /* supply a cookie (just for demonstration purposes) */
         fprintf (stdout, "100 SET-COOKIE AUTHAGENT_EXAMPLE=\"%s\"\n",
                  SoftwareId);
         fflush (stdout);
      }

      return;
   }
   if (!cptr) cptr = "Username unknown";

   /* doesn't matter what the string is, only the status code is checked */
   fprintf (stdout, "100 REASON %s\n", cptr);
   fflush (stdout);
   fprintf (stdout, "401 NOT authenticated\n");
   fflush (stdout);
}

/*****************************************************************************/
/*
Example group membership function.
*/

MemberOfGroup (char *UserNamePtr)

{
   static char  *ThreeStooges [] = { "moe", "larry", "curly", NULL };

   int  idx;

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

   if (Debug) fprintf (stdout, "MemberOfGroup() |%s|\n", UserNamePtr);

   if (DebugWatch)
   {
      /* just a comment that can be WATCHed */
      fprintf (stdout, "000 [%d] checking group membership of \"%s\"\n",
               __LINE__, UserNamePtr);
      fflush (stdout);
   }

   for (idx = 0; ThreeStooges[idx] != NULL; idx++)
   {
      if (!strsame (UserNamePtr, ThreeStooges[idx], -1)) continue;
      /* doesn't matter what the string is, only the status code is checked */
      fprintf (stdout, "200 YES\n");
      fflush (stdout);
      return;
   }

   /* doesn't matter what the string is, only the status code is checked */
   fprintf (stdout, "403 NO\n");
   fflush (stdout);
}

/*****************************************************************************/
/*
Example authentication from list kept in external file function. 
'ListFileNamePtr' ("WWW_AUTH_AGENT", from "param=") contains the file name. 
This simple example just looks for the user name and compares any plain-text
password it finds equated to it (e.g. "mark=daniel").  Any text that follows
the white-space delimited password is used as the user detail (.e.g
"mark=daniel Mark Daniel, Mark.Daniel@dsto.defence.gov.au").
*/

AuthenticateFromList
(
char *ListFileNamePtr,
char *UserNamePtr,
char *PasswordPtr
)
{
   register char  *sptr, *cptr;

   boolean  EndOfFile;
   int  idx;
   char  Line [256];
   FILE  *ListFile;

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

   if (Debug)
      fprintf (stdout, "AuthenticateFromList() |%s|%s|%s|\n",
               ListFileNamePtr, UserNamePtr, PasswordPtr);

   if (DebugWatch)
   {
      /* just a comment that can be WATCHed */
      fprintf (stdout, "000 [%d] authenticating \"%s\" from \"%s\"\n",
               __LINE__, UserNamePtr, ListFileNamePtr);
      fflush (stdout);
   }

   if (!ListFileNamePtr[0])
   {
      /* error string will be reported by the server */
      fprintf (stdout, "500 Database not specified.\n");
      return;
   }

   if ((ListFile = fopen (ListFileNamePtr, "r", "shr=get")) == NULL)
   {
      /* error string will be reported by the server */
      fprintf (stdout, "500 Database open error %%X%08.08X\n", vaxc$errno);
      return;
   }

   while (EndOfFile = (fgets (Line, sizeof(Line), ListFile) != NULL))
   {
      if (Debug) fprintf (stdout, "|%s|\n", Line);
      /* skip leading white-space */
      for (cptr = Line; *cptr && isspace(*cptr); cptr++);
      /* ignore comment lines */
      if (*cptr == '#' || *cptr == '!') continue;
      /* case sensitive comparison of user names */
      sptr = UserNamePtr;
      while (*cptr && *cptr != '=' && !isspace(*cptr) && *sptr)
      {
         if (*cptr != *sptr) break;
         cptr++;
         sptr++;
      }
      if (Debug) fprintf (stdout, "|%s|%s|\n", cptr, sptr);
      /* if not matched, continue */
      if (!*cptr || *cptr != '=' || *sptr) continue;
      /* case sensitive comparison of passwords */
      cptr++;
      sptr = PasswordPtr;
      while (*cptr && !isspace(*cptr) && *sptr)
      {
         if (*cptr != *sptr) break;
         cptr++;
         sptr++;
      }
      if (Debug) fprintf (stdout, "|%s|%s|\n", cptr, sptr);
      /* password verification succeeded! */
      if ((!*cptr || isspace(*cptr)) && !*sptr)
      {
         /* both the string and the status code are valid here */
         fprintf (stdout, "200 READ+WRITE\n");
         fflush (stdout);

         /* use anything that follows as the user detail */
         while (*cptr && isspace(*cptr)) cptr++;
         if (*cptr)
         {
            /* supply an informational - the user's details */
            for (sptr = cptr; *sptr && *sptr != '\n'; sptr++);
            *sptr = '\0';
            fprintf (stdout, "100 USER %s\n", cptr);
            fflush (stdout);
         }
         break;
      }
      /* doesn't matter what the string is, only the status code is checked */
      fprintf (stdout, "401 \"%s\" NOT authenticated\n", UserNamePtr);
      fflush (stdout);
      break;
   }
   EndOfFile = !EndOfFile;

   if (EndOfFile)
   {
      /* no such user name found */
      /* doesn't matter what the string is, only the status code is checked */
      fprintf (stdout, "401 \"%s\" NOT found\n", UserNamePtr);
      fflush (stdout);
   }

   fclose (ListFile);
}

/****************************************************************************/
/*
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.
*/ 

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

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

