/*****************************************************************************/
/*
                             authagent_CEL.c

WASD authentication agent with behaviour compatible with the OSU (DECthreads)
CEL authenticator (by Charles Lane).  Program contains no OSU-derived code.


Functional description from OSU [BASE_CODE]CEL_AUTHENTICATOR.C

 * The setup file simply defines a list of usernames/hosts and passwords.
 * If a password of '*' is specified, the corresponding username is checked
 * against the SYSUAF file.  If the username specification includes
 * a host address (e.g. SMITH@128.146.235.*), the password applies to only
 * the matching hosts.  A null username with a host address will accept
 * any connect from that host with no password check.
 *
 * Summary:
 *  <tag> tag-args...		   -> Special configuration info:
 *					<realm> string
 *  @host    *	   	           -> Host address must match
 *  username[@host] password       -> must match both
 *  *[@host]        password       -> any username, must match password
 *  username[@host] *              -> must match username, password from SYSUAF
 *  *[@host]        *              -> any username, password from SYSUAF
 *
 * If host restrictions would deny access regardless of username/password
 * given, the authentication fails with a 403 status so client doesn't waste
 * it's time prompting user for username/password.


DIFFERENCES WITH OSU CEL
------------------------
The OCU CEL authenticator is more rigid in it's processing of wildcards.  This
WASD implementation uses decc$match_wild() and so allows regular expressions in
the setup file strings.  This should not give rise to major differences in
behaviour.  The code also enforces a case-insensitive match.  For SYSUAF
authentication, accounts that are disusered, are captive or restricted, or have
SYSPRV authorized are always rejected.  This behaviour can be modified.  See
the "GLOBAL STORAGE" section below.  Comment-out any elements wished to be
allowed and recompile.  Caching is not needed by AUTHAGENT_CEL, as this is all
handled internally by WASD.  AUTHAGENT_CEL will only be accessed the first time
authorization is required, or after the user-revalidation period has expired.


HTTPD$AUTH CONFIGURATION
------------------------
CEL authenticator configuration.
Note that each path can have it's own setup file.

  ["CEL Authenticator"=AUTHAGENT_CEL=agent"]
  /some/path/or/other/*  r+w,param=HT_ROOT:[SRC.AGENT]AUTHAGENT_CEL.LIS
  /another/path/*        r+w,param=HT_ROOT:[LOCAL]AUTHAGENT_CEL.LIS


INSTALLED IMAGE
---------------
To access the SYSUAF and possibly protected setup files this program needs to
be installed with SYSPRV.

  $ INSTALL ADD HT_EXE:AUTHAGENT_CEL /OPEN /HEADER /SHARED /PRIV=SYSPRV

and have an ACL attached to prevent unexpected access:

  $ SET SECURITY HT_EXE:AUTHAGENT_CEL.EXE -
  /ACL=((IDENT=HTTP$SERVER,ACCESS=READ+EXEC),(IDENT=*,ACCESS=NONE))

If to be configured as part of a site's environment these two requirements
would need to be incorporated into the site Web service startup procedures.

As this image must be installed privileged it will require a DCL procedure
wrapper to activate it (all such image-installed privileged scripts require
this arrangement), HT_ROOT:[SCRIPT]AUTHAGENT_CEL.COM containing:

  $!(JUST ALLOWS IMAGE-INSTALLED PRIVILEGES TO BE ACTIVIATED)
  $ RUN HT_EXE:AUTHAGENT_CEL.EXE


LOGICAL NAMES
-------------
AUTHAGENT_CEL$DBUG        turns on all "if (Debug)" statements
AUTHAGENT_CEL$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_CEL
To just link:
  $ @BUILD_AUTHAGENT_CEL LINK


COPYRIGHT
---------
Copyright (C) 1999-2003 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 2.


VERSION HISTORY (update SOFTWAREVN as well)
---------------
23-DEC-2003  MGD  v1.1.1, minor conditional mods to support IA64
28-OCT-2000  MGD  v1.1.0, use CGILIB object module
31-OCT-1999  MGD  v1.0.0, initial development
*/

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

#define SOFTWAREVN "1.1.1"
#define SOFTWARENM "AUTHAGENT_CEL"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif

#ifndef __VAX
#   pragma nomember_alignment
#endif

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

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

#include <uaidef.h>
/* not defined in VAX C 3.2 <uaidef.h> */
#define UAI$M_RESTRICTED 0x8
#define UAI$C_PURDY_S 3

/* Internet-related header files */
#include <in.h>
#include <inet.h>
#include <netdb.h>
#include <socket.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

#define ISLWS(c) (c == ' ' || c == '\t')
#define ISEOL(c) (c == '\0' || c == '\n')

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

/* UAI flags that disallow SYSUAF authentication */
unsigned long  DisallowVmsFlags = UAI$M_DISACNT |
                                  UAI$M_PWD_EXPIRED |
                                  UAI$M_PWD2_EXPIRED |
                                  UAI$M_CAPTIVE |
                                  UAI$M_RESTRICTED | 0;

/* authorized privileges that disallow SYSUAF authentication */
unsigned long  DisallowVmsPrivileges = PRV$M_SYSPRV | 0;

char  Utility [] = "AUTHAGENT_CEL";

boolean  Debug,
         DebugWatch;
         
char  SoftwareID [48];

unsigned long  SysPrvMask [2] = { PRV$M_SYSPRV, 0 };

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

main ()
       
{
   int  status;

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

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

   CgiLibEnvironmentInit (0, NULL, false);

   /* MUST only be executed in a CGIplus environment! */
   if (!CgiLibEnvironmentIsCgiPlus ()) exit (SS$_ABORT);

   for (;;)
   {
      /* block waiting for the next request */
      CgiLibVar ("");
      ProcessRequest ();
      CgiLibCgiPlusEOF ();
   }
}

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

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

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

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

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

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

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

   UserNamePtr = CgiLibVar ("WWW_REMOTE_USER");
   PasswordPtr = CgiLibVar ("WWW_AUTH_PASSWORD");

   AuthorizeUser (AuthAgentPtr, UserNamePtr, PasswordPtr);

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

/*****************************************************************************/
/*
Open the CEL setup file.  Read each line parsing the authorization components
out of those  that contain setup data.  Then process the username, password and
host restriction components to return true if access is allowed, or false
otherwise.  'AuthAgentPtr' will contain the "param=" specified setup file name.
*/

AuthorizeUser
(
char *AuthAgentPtr,
char *UserNamePtr,
char *PasswordPtr
)
{
   register char  *cptr, *sptr, *zptr; 

   boolean  AccessOk;
   int  status,
        LineCount;
   char  Line [256],
         CelRealm [64],
         CelHost [128],
         CelPassword [64],
         CelUserName [64];
   FILE  *CelFile;

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

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

   if (DebugWatch)
   {
      fprintf (stdout, "000 [%d] authorizing \"%s\" from \"%s\"\n",
               __LINE__, UserNamePtr, AuthAgentPtr);
      fflush (stdout);
   }

   /* turn on SYSPRV to allow access to a possibly protected file */
   status = sys$setprv (1, &SysPrvMask, 0, 0);
   if (VMSnok(status) || status == SS$_NOTALLPRIV)
   {
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] sys$setprv() %%X%08.08X\n",
                  __LINE__, status);
         fflush (stdout);
      }
      fprintf (stdout, "500 Error %%X%08.08X\n", status & 0xfffffffe);
      fflush (stdout);
      return;
   }

   CelFile = fopen (AuthAgentPtr, "r", "shr=get");

   /* turn off SYSPRV */
   if (VMSnok (status = sys$setprv (0, &SysPrvMask, 0, 0)))
      exit (status);

   if (CelFile == NULL)
   {
      status = vaxc$errno;
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] error %%X%08.08X opening file \"%s\"\n",
                  __LINE__, status, AuthAgentPtr);
         fflush (stdout);
      }
      fprintf (stdout, "500 Error %%X%08.08X  opening file.\n", status);
      fflush (stdout);
      return;
   }

   CelRealm[0] = '\0';
   LineCount = 0;

   /*************/
   /* read file */
   /*************/

   while (fgets (Line, sizeof(Line), CelFile) != NULL)
   {
      LineCount++;
      if (DebugWatch)
      {
         for (cptr = Line; !ISEOL(*cptr); cptr++);
         *cptr = '\0';
         fprintf (stdout, "000 [%d] line %d \"%s\"\n",
                  __LINE__, LineCount, Line);
         fflush (stdout);
      }
      for (cptr = Line; !ISEOL(*cptr) && ISLWS(*cptr); cptr++);
      if (ISEOL(*cptr)) continue;
      if (*cptr == '#') continue;

      CelHost[0] = CelPassword[0] = CelUserName[0] = '\0';

      if (strsame (cptr, "<realm>", 7))
      {
         /*************/
         /* realm tag */
         /*************/

         for (cptr += 7; *cptr && ISLWS(*cptr); cptr++);
         zptr = (sptr = CelRealm) + sizeof(CelRealm)-1;
         while (!ISEOL(*cptr) && sptr < zptr) *sptr++ = *cptr++;
         *sptr = '\0';
         if (Debug) fprintf (stdout, "CelRealm |%s|\n", CelRealm);
         continue;
      }

      /************/
      /* username */
      /************/

      if (*cptr == '*')
      {
         *(unsigned short*)CelUserName = '*\0';
         while (*cptr == '*') cptr++;
         while (*cptr && ISLWS(*cptr)) cptr++;
      }
      if (*cptr != '@')
      {
         /* not a restriction host, must be username */
         zptr = (sptr = CelUserName) + sizeof(CelUserName)-1;
         while (!ISEOL(*cptr) && !ISLWS(*cptr) && *cptr != '@' && sptr < zptr)
            *sptr++ = *cptr++;
         *sptr = '\0';
         if (Debug) fprintf (stdout, "CelUserName |%s|\n", CelUserName);
         while (*cptr && ISLWS(*cptr)) cptr++;
      }

      /*********************/
      /* host name/address */
      /*********************/

      if (*cptr == '@')
      {
         /* restriction host name/address */
         cptr++;
         zptr = (sptr = CelHost) + sizeof(CelHost)-1;
         /* force to lower case (for case-less comparison) */
         while (!ISEOL(*cptr) && !ISLWS(*cptr) && sptr < zptr)
            *sptr++ = tolower(*cptr++);
         *sptr = '\0';
         if (Debug) fprintf (stdout, "CelHost |%s|\n", CelHost);
         while (*cptr && ISLWS(*cptr)) cptr++;
      }

      /******************/
      /* authentication */
      /******************/

      /* now the authentication source (or password) */
      zptr = (sptr = CelPassword) + sizeof(CelPassword)-1;
      while (!ISEOL(*cptr) && !ISLWS(*cptr) && sptr < zptr)
         *sptr++ = *cptr++;
      *sptr = '\0';
      if (Debug) fprintf (stdout, "CelPassword |%s|\n", CelPassword);

      /***********/
      /* process */
      /***********/

      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] authorize \"%s\"@\"%s\" \"%s\"\n",
                  __LINE__, CelUserName, CelHost, CelPassword);
         fflush (stdout);
      }

      if (!CelPassword[0] ||
          (!CelUserName[0] && !CelHost[0]))
      {
         /* must have a password, cannot have no username and no host */
         if (DebugWatch)
         {
            fprintf (stdout, 
               "000 [%d] configuration problem at line %d of \"%s\"\n",
               __LINE__, LineCount, AuthAgentPtr);
            fflush (stdout);
         }
         continue;
      }

      /* if any literal username doesn't match then process next line */
      if (CelUserName[0] &&
          CelUserName[0] != '*' &&
          !strsame (UserNamePtr, CelUserName, -1))
         continue;

      if (CelHost[0] && !MatchHost(CelHost))
      {
         /* does not match the host requirement, immediate 403 */
         if (DebugWatch)
         {
            fprintf (stdout,
               "000 [%d] restricted by host at line %d of \"%s\"\n",
               __LINE__, LineCount, AuthAgentPtr);
            fflush (stdout);
         }
         fprintf (stdout, "403 Forbidden.\n");
         fflush (stdout);
         fclose (CelFile);
         return;
      }

      if (!CelUserName[0] ||
          (CelPassword[0] != '*' && strsame (PasswordPtr, CelPassword, -1)) ||
          (CelPassword[0] == '*' && MatchSysUaf (UserNamePtr, PasswordPtr)))
      {
         /* no username, or literal password or SYSUAF password match */
         fprintf (stdout, "200 READ+WRITE\n");
         fflush (stdout);
         fclose (CelFile);
         return;
      }

      /* host OK, but not authenticated, return a 401 status */
      break;
   }

   fclose (CelFile);

   /*******************/
   /* nothing matched */
   /*******************/

   if (CelRealm[0])
      fprintf (stdout, "401 \"%s\"\n", CelRealm);
   else
      fprintf (stdout, "401 Not authenticated\n");
   fflush (stdout);
}

/*****************************************************************************/
/*
Wildcard or literal match of client host address against that specified in the
CEL setup file.  If the host specified is an alpha-numeric then compare it to
the server-supplied host name, and if this is numeric because DNS lookup is
disabled then lookup the address and use the results.  Return true if match is
OK, false otherwise.
*/

boolean MatchHost (char *CelHost)
       
{
   register char  *cptr; 

   int  status,
        IpAddress;
   char  *RemoteAddrPtr,
         *RemoteHostPtr;
   struct hostent  *HostEntryPtr;

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

   if (Debug) fprintf (stdout, "RestrictOnHost() |%s|\n", CelHost);

   if (DebugWatch)
   {
      fprintf (stdout, "000 [%d] match host \"%s\"\n", __LINE__, CelHost);
      fflush (stdout);
   }

   for (cptr = CelHost; *cptr && !isalpha(*cptr); cptr++);
   if (*cptr)
   {
      /* alpha-numeric host specification */
      RemoteHostPtr = CgiLibVar ("WWW_REMOTE_HOST");

      for (cptr = RemoteHostPtr; *cptr && !isalpha(*cptr); cptr++);
      if (*cptr)
      {
         /* server DNS resolution turned on, just use host name */
         if (DebugWatch)
         {
            fprintf (stdout, "000 [%d] match \"%s\" by \"%s\"\n",
                     __LINE__, RemoteHostPtr, CelHost);
            fflush (stdout);
         }
         return (decc$match_wild (RemoteHostPtr, CelHost));
      }

      /* need to convert the address into a host name */
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] resolve host address \"%s\"\n",
                  __LINE__, RemoteHostPtr);
         fflush (stdout);
      }
      IpAddress = inet_addr (RemoteHostPtr);
      if (Debug) fprintf (stdout, "IpAddress: %08.08X\n", IpAddress);
      if (IpAddress == -1) return (false);
      if ((HostEntryPtr =
          gethostbyaddr (&IpAddress, sizeof(IpAddress), AF_INET)) == NULL)
      {
         if (DebugWatch)
         {
            fprintf (stdout, "000 [%d] host address \"%s\" not resolved\n",
                     __LINE__, RemoteHostPtr);
            fflush (stdout);
         }
         return (false);
      }

      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] match \"%s\" by \"%s\"\n",
                  __LINE__, HostEntryPtr->h_name, CelHost);
         fflush (stdout);
      }
      return (decc$match_wild (HostEntryPtr->h_name, CelHost));
   }
   else
   {
      /* no alphas, presume dotted-decimal address */
      RemoteAddrPtr = CgiLibVar ("WWW_REMOTE_ADDR");
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] match \"%s\" by \"%s\"\n",
                  __LINE__, RemoteAddrPtr, CelHost);
         fflush (stdout);
      }
      return (decc$match_wild (RemoteAddrPtr, CelHost));
   }
}

/*****************************************************************************/
/*
Verify the request username/password from the on-disk SYSUAF database.

Get the specified user's flags, authorized privileges, quadword password, hash
salt and encryption algorithm from SYSUAF.  If any of specified bits in the
flags are set (e.g. "disusered") then fail the password authentication.
Using the salt and encryption algorithm hash the supplied password and compare
it to the UAF hashed password.  A SYSPRV privileged account is always failed.

Return true if the password is validated and there are no account restrictions,
false otherwise.
*/ 

boolean MatchSysUaf
(
char *UserNamePtr,
char *PasswordPtr
)
{
   static unsigned long  Context = -1;

   static unsigned long  UaiFlags,
                         UaiUic;
   static unsigned long  UaiPriv [2],
                         HashedPwd [2],
                         UaiPwd [2];
   static unsigned short  UaiSalt;
   static unsigned char  UaiEncrypt;

   static char UserName [15+1],
               Password [39+1];
   static $DESCRIPTOR (UserNameDsc, UserName);
   static $DESCRIPTOR (PasswordDsc, Password);

   static struct {
      short BufferLength;
      short ItemCode;
      void  *BufferPtr;
      void  *LengthPtr;
   } UaiItems [] = 
   {
      { sizeof(UaiUic), UAI$_UIC, &UaiUic, 0 },
      { sizeof(UaiFlags), UAI$_FLAGS, &UaiFlags, 0 },
      { sizeof(UaiPriv), UAI$_PRIV, &UaiPriv, 0 },
      { sizeof(UaiPwd), UAI$_PWD, &UaiPwd, 0 },
      { sizeof(UaiEncrypt), UAI$_ENCRYPT, &UaiEncrypt, 0 },
      { sizeof(UaiSalt), UAI$_SALT, &UaiSalt, 0 },
      { 0, 0, 0, 0 }
   };

   register char  *cptr, *sptr, *zptr;

   int  status,
        setprvStatus;

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

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

   /* to upper case! (just truncate if too long) */
   zptr = (sptr = UserName) + sizeof(UserName)-1;
   for (cptr = UserNamePtr; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   UserNameDsc.dsc$w_length = sptr - UserName;

   if (DebugWatch)
   {
      fprintf (stdout, "000 [%d] authenticate \"%s\" from SYSUAF\n",
               __LINE__, UserName);
      fflush (stdout);
   }

   /* turn on SYSPRV to allow access to SYSUAF records */
   setprvStatus = sys$setprv (1, &SysPrvMask, 0, 0);
   if (VMSnok(setprvStatus) || setprvStatus == SS$_NOTALLPRIV)
   {
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] sys$setprv() %%X%08.08X\n",
                  __LINE__, setprvStatus);
         fflush (stdout);
      }
      fprintf (stdout, "500 Error %%X%08.08X\n", setprvStatus & 0xfffffffe);
      fflush (stdout);
      return (false);
   }

   status = sys$getuai (0, &Context, &UserNameDsc, &UaiItems, 0, 0, 0);
   if (Debug) fprintf (stdout, "sys$getuai() %%X%08.08X\n", status);

   /* turn off SYSPRV */
   if (VMSnok (setprvStatus = sys$setprv (0, &SysPrvMask, 0, 0)))
      exit (setprvStatus);

   if (VMSnok (status)) 
   {
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] sys$getuai() %%X%08.08X\n",
                  __LINE__, status);
         fflush (stdout);
      }
      return (false);
   }

   /* automatically disallow if any of these flags are set! */
   if (UaiFlags & DisallowVmsFlags)
   {
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] failed on SYSUAF flags\n", __LINE__);
         fflush (stdout);
      }
      return (false);
   }

   if (UaiPriv[0] & DisallowVmsPrivileges)
   {
      /* exclude accounts with extended privileges */
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] failed on SYSUAF privileges\n", __LINE__);
         fflush (stdout);
      }
      return (false);
   }

   /* to upper case! (just truncate if too long) */
   zptr = (sptr = Password) + sizeof(Password)-1;
   for (cptr = PasswordPtr; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   PasswordDsc.dsc$w_length = sptr - Password;

   status = sys$hash_password (&PasswordDsc, UaiEncrypt,
                               UaiSalt, &UserNameDsc, &HashedPwd);
   if (Debug) fprintf (stdout, "sys$hash_password() %%X%08.08X\n", status);
   if (VMSnok (status))
   {                            
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] sys$hash_password() %%X%08.08X\n",
                  __LINE__, status);
         fflush (stdout);
      }
      return (false);
   }

   if (HashedPwd[0] != UaiPwd[0] || HashedPwd[1] != UaiPwd[1])
   {
      if (DebugWatch)
      {
         fprintf (stdout, "000 [%d] failed on SYSUAF password\n", __LINE__);
         fflush (stdout);
      }
      return (false);
   }

   return (true);
}

/****************************************************************************/
/*
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
(
register char *sptr1,
register char *sptr2,
register 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);
}

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

