/*****************************************************************************/
/*
                               AuthHTA.c

Licensed under the Apache License, Version 2.0 (the License);
you may not use this software 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.

This module provides functions related to the authentication of usernames via
the WASD HTA databases (maintained via HTADMIN.C).

See AUTH.C for overall detail on the WASD authorization environment.


VERSION HISTORY
---------------
21-MAY-2021  MGD  keep HTA record structure as-was (using unsigned longs)
                    and munge for access to 64 bit time
30-MAY-2007  MGD  allow for CONNECT method (basically as a 'write' flag)
28-SEP-2003  MGD  HTA database now "read [record] regardless of lock"
26-AUG-2003  MGD  service directory located authorization databases
27-APR-2002  MGD  use sys$setprv()
04-AUG-2001  MGD  support module WATCHing
12-DEC-2000  MGD  username size now HTA-specific, different to '->RemoteUser'
02-JAN-2000  MGD  no significant modifications for ODS-5 (no NAM block)
28-AUG-1999  MGD  unbundled from AUTH.C for v6.1
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

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

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

#include <uaidef.h>
/* not defined VAX C 3.2 <uaidef.h> (probably unnecessary now 11-MAY-2005) */
#ifndef UAI$C_PURDY_S
#define UAI$C_PURDY_S 3
#endif 

/* application related header files */
#include "wasd.h"

#define WASD_MODULE "AUTHHTA"

#if WATCH_MOD
#define FI_NOLI WASD_MODULE, __LINE__
#else
/* in production let's keep the exact line to ourselves! */
#define FI_NOLI WASD_MODULE, 0
#endif

/********************/
/* external storage */
/********************/

extern int  ServerPort;

extern unsigned long  SysPrvMask[];

extern char  *AuthConfigHtaDirectory;

extern char  ErrorSanityCheck[],
             SoftwareID[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern MSG_STRUCT  Msgs;
extern WATCH_STRUCT  Watch;

/****************************************************************************/
/*
Verify the request username/password and/or read capabilities from the on-disk
HTA (HTTPd) authorization database.

Returns a success status if user password authenticated,
AUTH_DENIED_BY_LOGIN if not authenticated, or other error status if a
genuine VMS error occurs (which should be reported where and when it occurs).

The digest is stored in both upper and lower case versions because clients
will have case-lock either on or off producing a digest of either upper or
lower case username and password.  Keep digests of both so that provided one
or other case is used exclusively the digest thereof can still be matched
with one or other stored here :^)
*/ 

int AuthReadHtDatabase
(
REQUEST_STRUCT* rqptr,
char *DatabaseName,
BOOL AuthenticatePassword
)
{
   static char  Password [AUTH_MAX_PASSWORD_LENGTH+1],
                UserName [AUTH_MAX_HTA_USERNAME_LENGTH+1];
   static $DESCRIPTOR (PasswordDsc, Password);
   static $DESCRIPTOR (UserNameDsc, UserName);

   BOOL  PasswordAuthenticated,
         UserNameEnabled;
   int  status;
   unsigned long  HashedPwd [2];
   unsigned long  *BeginTimePtr;
   char  *cptr, *sptr, *zptr;
   char  A1HexDigest [33],
         HexDigest [33];
   AUTH_HTAREC AuthHtRecord;

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

   if (WATCHMOD (rqptr, WATCH_MOD_AUTH))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH,
                 "AuthReadHtDatabase() !&Z !&Z !UL",
                 DatabaseName, rqptr->RemoteUser, AuthenticatePassword);

   /* flag that case-less username and password checks were performed */
   rqptr->rqAuth.CaseLess = true;

   /* set the capabilities to zero before we do anything else! */
   rqptr->rqAuth.UserCan = 0;

   /* to uppercase! */
   zptr = (sptr = UserName) + sizeof(UserName)-1;
   for (cptr = rqptr->RemoteUser;
        *cptr && sptr < zptr;
        *sptr++ = to_upper(*cptr++));
   *sptr = '\0';
   UserNameDsc.dsc$w_length = sptr - UserName;

   /* look for the record, leave the database file open if found */
   status = AuthHtDatabaseAccess (rqptr, true, DatabaseName,
                                  UserName, &AuthHtRecord, NULL, NULL);
   if (status == RMS$_EOF)
   {
      if (WATCHING (rqptr, WATCH_AUTH))
         WatchThis (WATCHITM(rqptr), WATCH_AUTH,
                    "FAIL !AZ!AZ username", DatabaseName,
                    AuthSourceString (DatabaseName, AUTH_SOURCE_HTA));

      /* close the database */
      AuthHtDatabaseAccess (NULL, false, NULL, NULL, NULL, NULL, NULL);

      if (AuthenticatePassword)
         return (AUTH_DENIED_BY_LOGIN);
      else
         return (AUTH_DENIED_BY_GROUP);
   }

   if (VMSnok (status))
   {
      rqptr->rqResponse.ErrorTextPtr = MsgFor(rqptr,MSG_AUTH_DATABASE);
      rqptr->rqResponse.ErrorOtherTextPtr = DatabaseName;
      ErrorVmsStatus (rqptr, status, FI_LI);
      return (status);
   }

   /****************/
   /* record found */
   /****************/

   PasswordAuthenticated = false;

   if (AuthHtRecord.Flags & AUTH_FLAG_ENABLED)
   {
      UserNameEnabled = true;
      if (AuthenticatePassword &&
          AuthHtRecord.HashedPwd[0] && AuthHtRecord.HashedPwd[1])
      {
         if (rqptr->rqAuth.Scheme == AUTH_SCHEME_BASIC)
         {
            /* to uppercase! */
            zptr = (sptr = Password) + sizeof(Password)-1;
            for (cptr = rqptr->RemoteUserPassword;
                 *cptr && sptr < zptr;
                 *sptr++ = to_upper(*cptr++));
            *sptr = '\0';
            PasswordDsc.dsc$w_length = sptr - Password;

            status = sys$hash_password (&PasswordDsc, UAI$C_PURDY_S, 0,
                                        &UserNameDsc, &HashedPwd);
            if (VMSnok (status))
            {
               /* ensure the currently open database is closed */
               AuthHtDatabaseAccess (NULL, false, NULL, NULL,
                                     NULL, NULL, NULL);
               rqptr->rqResponse.ErrorTextPtr = MsgFor(rqptr,MSG_AUTH_USER);
               ErrorVmsStatus (rqptr, status, FI_LI);
               return (status);
            }

            if (WATCHPNT(rqptr) &&
                WATCH_MODULE(WATCH_MOD_AUTH))
                WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH, "!8XL!8XL !8XL!8XL",
                           HashedPwd[1], HashedPwd[0],
                           AuthHtRecord.HashedPwd[1],
                           AuthHtRecord.HashedPwd[0]);

            if (HashedPwd[0] == AuthHtRecord.HashedPwd[0] &&
                HashedPwd[1] == AuthHtRecord.HashedPwd[1])
               PasswordAuthenticated = true;
         }
         else
         if (rqptr->rqAuth.Scheme == AUTH_SCHEME_DIGEST)
         {
            DigestHexString (AuthHtRecord.A1DigestLoCase, 16, A1HexDigest);
            DigestResponse (A1HexDigest,
                            rqptr->rqAuth.DigestNoncePtr,
                            rqptr->rqHeader.MethodName,
                            rqptr->rqAuth.DigestUriPtr,
                            HexDigest);

            if (WATCHPNT(rqptr) &&
                WATCH_MODULE(WATCH_MOD_AUTH))
            {
                WatchDataDump (AuthHtRecord.A1DigestLoCase, 16);
                WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH, "!&Z !&Z",
                           rqptr->rqAuth.DigestResponsePtr, HexDigest);
            }

            if (!strcmp (rqptr->rqAuth.DigestResponsePtr, HexDigest))
               PasswordAuthenticated = true;
            else
            {
               DigestHexString (AuthHtRecord.A1DigestUpCase, 16, A1HexDigest);
               DigestResponse (A1HexDigest,
                               rqptr->rqAuth.DigestNoncePtr,
                               rqptr->rqHeader.MethodName,
                               rqptr->rqAuth.DigestUriPtr,
                               HexDigest);

               if (WATCHPNT(rqptr) &&
                   WATCH_MODULE(WATCH_MOD_AUTH))
               {
                   WatchDataDump (AuthHtRecord.A1DigestUpCase, 16);
                   WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH, "!&Z !&Z",
                              rqptr->rqAuth.DigestResponsePtr, HexDigest);
               }

               if (!strcmp (rqptr->rqAuth.DigestResponsePtr, HexDigest))
                  PasswordAuthenticated = true;
            }
         }

         if (!PasswordAuthenticated)
            if (WATCHING (rqptr, WATCH_AUTH))
               WatchThis (WATCHITM(rqptr), WATCH_AUTH,
                         "FAIL !AZ!AZ password", DatabaseName,
                         AuthSourceString (DatabaseName, AUTH_SOURCE_HTA));

         if (WATCHMOD (rqptr, WATCH_MOD_AUTH))
             WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH, "AUTHENTICATED");
      }

      if (!AuthenticatePassword || PasswordAuthenticated)
      {
         /* set the appropriate authorization flags */
         if (AuthHtRecord.Flags & AUTH_FLAG_CONNECT)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_CONNECT;
         if (AuthHtRecord.Flags & AUTH_FLAG_DELETE)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_DELETE;
         if (AuthHtRecord.Flags & AUTH_FLAG_GET)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_GET | HTTP_METHOD_HEAD;
         if (AuthHtRecord.Flags & AUTH_FLAG_HEAD)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_HEAD;
         if (AuthHtRecord.Flags & AUTH_FLAG_POST)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_POST;
         if (AuthHtRecord.Flags & AUTH_FLAG_PUT)
            rqptr->rqAuth.UserCan |= HTTP_METHOD_PUT;

         if (AuthHtRecord.Flags & AUTH_FLAG_HTTPS_ONLY)
            rqptr->rqAuth.HttpsOnly = true;
         else
            rqptr->rqAuth.HttpsOnly = false;
      }
   }

   BeginTimePtr = &rqptr->rqTime.BeginTime64;
   if (UserNameEnabled && (!AuthenticatePassword || PasswordAuthenticated))
   {
      AuthHtRecord.AccessCount++;
      AuthHtRecord.LastAccessTime64[0] = BeginTimePtr[0];
      AuthHtRecord.LastAccessTime64[1] = BeginTimePtr[1];
   }
   else
   {
      AuthHtRecord.FailureCount++;
      AuthHtRecord.LastFailureTime64[0] = BeginTimePtr[0];
      AuthHtRecord.LastFailureTime64[1] = BeginTimePtr[1];
   }

   /* update the record, close the database file */
   status = AuthHtDatabaseAccess (NULL, false, NULL, NULL,
                                  NULL, NULL, &AuthHtRecord);
   if (VMSnok (status))
   {
      rqptr->rqResponse.ErrorTextPtr = MsgFor(rqptr,MSG_AUTH_DATABASE);
      rqptr->rqResponse.ErrorOtherTextPtr = DatabaseName;
      ErrorVmsStatus (rqptr, status, FI_LI);
      return (status);
   }

   if (!AuthenticatePassword || PasswordAuthenticated)
   {
      if (AuthenticatePassword)
      {
         int  Length;
         char  *cptr, *sptr;

         Length = 0;
         if (AuthHtRecord.FullName[0])
            Length += strlen(AuthHtRecord.FullName);
         if (AuthHtRecord.Email[0])
         {
            if (Length) Length++;
            Length += strlen(AuthHtRecord.Email);
         }
         if (AuthHtRecord.Contact[0])
         {
            if (Length) Length++;
            Length += strlen(AuthHtRecord.Contact);
         }

         if (Length)
         {
            rqptr->rqAuth.UserDetailsLength = Length;
            rqptr->rqAuth.UserDetailsPtr = sptr =
               VmGetHeap (rqptr, rqptr->rqAuth.UserDetailsLength+1);
            if (AuthHtRecord.FullName[0])
               for (cptr = AuthHtRecord.FullName; *cptr; *sptr++ = *cptr++);
            if (AuthHtRecord.Email[0])
            {
               if (sptr > rqptr->rqAuth.UserDetailsPtr) *sptr++ = '\n';
               for (cptr = AuthHtRecord.Email; *cptr; *sptr++ = *cptr++);
            }
            if (AuthHtRecord.Contact[0])
            {
               if (sptr > rqptr->rqAuth.UserDetailsPtr) *sptr++ = '\n';
               for (cptr = AuthHtRecord.Contact; *cptr; *sptr++ = *cptr++);
            }
            *sptr = '\0';
         }
      }

      return (SS$_NORMAL);
   }
   else
   if (AuthenticatePassword)
      return (AUTH_DENIED_BY_LOGIN);
   else
      return (AUTH_DENIED_BY_GROUP);
}

/****************************************************************************/
/*
Locate a specified user record in a specified on-disk database.  Either return
that record if found or if an update record supplied, update it! Can return any
RMS or VMS error status code.  Returns SS$_NORMAL is operation (read, add or
update) completed successfully.  Returns AUTH_DENIED_BY_LOGIN if the record
could not be found.  Returns SS$_INCOMPAT if the database file version is
incorrect.

THE DATABASE FILE IS ALWAYS CLOSED ON AN ERROR CONDITION.

'LeaveFileOpen' will leave the database file open with the current record
context ready for update, or for locating another user record.  This function
is not reentrant and therefore this context is valid ONLY within the one AST.

Call with 'DatabaseName' and 'UserName' non-NULL and all other pointers set to
NULL to check whether the user name has a record in the database.
*/ 

int AuthHtDatabaseAccess
(
REQUEST_STRUCT* rqptr,
BOOL LeaveFileOpen,
char *DatabaseName,
char *UserName,
AUTH_HTAREC *AuthHtRecordReadPtr,
AUTH_HTAREC *AuthHtRecordAddPtr,
AUTH_HTAREC *AuthHtRecordUpdatePtr
)
{
   static struct FAB  AuthFileFab;
   static struct RAB  AuthFileRab;

   int  status,
        AuthFileNameLength,
        UserNameLength;
   char  *cptr, *sptr, *zptr;
   char  AuthFileName [256];
   AUTH_HTAREC  *AuthHtRecordPtr;
   AUTH_HTAREC  AuthHtRecord;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH, 
                 "AuthHtDatabaseAccess() !&X !&B !&Z !&Z !&X !&X !&X",
                  rqptr, LeaveFileOpen, DatabaseName, UserName,
                  AuthHtRecordReadPtr, AuthHtRecordAddPtr,
                  AuthHtRecordUpdatePtr);

   if (!AuthHtRecordReadPtr && !AuthHtRecordAddPtr && !AuthHtRecordUpdatePtr)
   {
      /************************/
      /* force database close */
      /************************/

      sys$close (&AuthFileFab, 0, 0);
      return (SS$_NORMAL);
   }

   if (!UserName && AuthHtRecordUpdatePtr)
   {
      /*********************************/
      /* update previously read record */
      /*********************************/

      AuthFileRab.rab$l_rbf = AuthHtRecordUpdatePtr;
      AuthFileRab.rab$w_rsz = sizeof(AUTH_HTAREC);
      status = sys$update (&AuthFileRab, 0, 0);
      if (VMSok (status))
      {
         /* update completed successfully */
         if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
         return (SS$_NORMAL);
      }
      /* update error */
      sys$close (&AuthFileFab, 0, 0);
      return (status);
   }

   if (!DatabaseName)
   {
      /*****************************************/
      /* locate within currently open database */
      /*****************************************/

      if (VMSnok (status = sys$rewind (&AuthFileRab, 0, 0)))
      {
         sys$close (&AuthFileFab, 0, 0);
         return (status);
      }
   }
   else
   {
      /**********************************/
      /* open currently closed database */
      /**********************************/

      if (rqptr->rqAuth.PathParameterPtr &&
          rqptr->rqAuth.PathParameterPtr[0])
      {
         if ((cptr = strstr (rqptr->rqAuth.PathParameterPtr, "/directory=")) ||
             (cptr = strstr (rqptr->rqAuth.PathParameterPtr, "/DIRECTORY=")))
            cptr += 11;
         else
             cptr = NULL;
      }
      else
      if (rqptr->rqAuth.DirectoryPtr)
         cptr = rqptr->rqAuth.DirectoryPtr;
      else
      if (rqptr->ConfigDirectory[0])
         cptr = rqptr->ConfigDirectory;
      else
         cptr = AuthConfigHtaDirectory;

      /* just a safeguard against the service directory not being configured */
      if (!*cptr) cptr = AUTH_DIR_NOT_CONFIGURED;

      zptr = (sptr = AuthFileName) + sizeof(AuthFileName)-1;
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      for (cptr = DatabaseName; *cptr && sptr < zptr; *sptr++ = *cptr++);
      for (cptr = HTA_FILE_TYPE; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      AuthFileNameLength = sptr - AuthFileName;

      if (WATCHING (rqptr, WATCH_AUTH))
         WatchThis (WATCHITM(rqptr), WATCH_AUTH, "FILE !AZ", AuthFileName);

      AuthFileFab = cc$rms_fab;
      /* if a read-only operation without file left open then read-only! */
      if (!LeaveFileOpen && !AuthHtRecordAddPtr && !AuthHtRecordUpdatePtr)
         AuthFileFab.fab$b_fac = FAB$M_GET;
      else
         AuthFileFab.fab$b_fac = FAB$M_GET | FAB$M_PUT | FAB$M_UPD;
      AuthFileFab.fab$l_fna = AuthFileName;  
      AuthFileFab.fab$b_fns = AuthFileNameLength;
      AuthFileFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT | FAB$M_SHRUPD;

      /* turn on SYSPRV to allow access to authentication database file */
      sys$setprv (1, &SysPrvMask, 0, 0);

      status = sys$open (&AuthFileFab, 0, 0);

      sys$setprv (0, &SysPrvMask, 0, 0);

      /* status from sys$open() */
      if (VMSnok (status)) return (status);

      AuthFileRab = cc$rms_rab;
      AuthFileRab.rab$l_fab = &AuthFileFab;
      /* 2 buffers of sixty-four blocks (records) each */
      AuthFileRab.rab$b_mbc = 64;
      AuthFileRab.rab$b_mbf = 2;
      /* read-ahead and read regardless of lock options */
      AuthFileRab.rab$l_rop = RAB$M_RAH | RAB$M_RRL;

      if (VMSnok (status = sys$connect (&AuthFileRab, 0, 0)))
      {
         sys$close (&AuthFileFab, 0, 0);
         return (status);
      }
   }

   /*****************/
   /* locate record */
   /*****************/

   /* for add and update read into the scratch record */
   if (!AuthHtRecordReadPtr)
      AuthFileRab.rab$l_ubf = AuthHtRecordPtr = &AuthHtRecord;
   else
      AuthFileRab.rab$l_ubf = AuthHtRecordPtr = AuthHtRecordReadPtr;
   AuthFileRab.rab$w_usz = sizeof(AUTH_HTAREC);

   if (UserName) UserNameLength = strlen(UserName);

   while (VMSok (status = sys$get (&AuthFileRab, 0, 0)))
   {
      /* check the version of the authorization database */
      if (AuthHtRecordPtr->DatabaseVersion &&
          AuthHtRecordPtr->DatabaseVersion != AUTH_HTA_VERSION)
      {
         status = SS$_INCOMPAT & 0xfffffffe;
         break;
      }

      /* if deleted record (all set to zeroes) continue */
      if (!AuthHtRecordAddPtr && !AuthHtRecordPtr->UserNameLength) continue;

      /* if adding a record then use the first deleted one found */
      if (AuthHtRecordAddPtr && !AuthHtRecordPtr->UserNameLength) break;
      if (UserNameLength != AuthHtRecordPtr->UserNameLength) continue;
      cptr = AuthHtRecordPtr->UserName;
      sptr = UserName;
      while (*cptr && *sptr && to_lower(*cptr) == to_lower(*sptr))
         { cptr++; sptr++; }
      if (*cptr || *sptr) continue;
      break;
   }

   if (!AuthHtRecordUpdatePtr && !AuthHtRecordAddPtr)
   {
      /***************/
      /* read record */
      /***************/

      if (VMSok (status))
      {
         /* record found */
         if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
         return (SS$_NORMAL);
      }

      if (status == RMS$_EOF)
      {
         /* user record not found */
         if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
         return (status);
      }
      /* RMS error */
      sys$close (&AuthFileFab);
      return (status);
   }

   if (AuthHtRecordUpdatePtr)
   {
      /*****************/
      /* update record */
      /*****************/

      if (VMSnok (status))
      {
         if (status == RMS$_EOF)
         {
            /* user record not found */
            if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
            return (status);
         }
         /* RMS error */
         sys$close (&AuthFileFab, 0, 0);
         return (status);
      }

      AuthFileRab.rab$l_rbf = AuthHtRecordUpdatePtr;
      AuthFileRab.rab$w_rsz = sizeof(AUTH_HTAREC);

      status = sys$update (&AuthFileRab, 0, 0);
      if (VMSok (status))
      {
         /* update completed successfully */
         if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
         return (SS$_NORMAL);
      }
      /* RMS error */
      sys$close (&AuthFileFab, 0, 0);
      return (status);
   }

   if (AuthHtRecordAddPtr)
   {
      /****************/
      /* add a record */
      /****************/

      if (VMSnok (status) && status != RMS$_EOF)
      {
         /* RMS error */
         sys$close (&AuthFileFab, 0, 0);
         return (status);
      }

      AuthFileRab.rab$l_rbf = AuthHtRecordAddPtr;
      AuthFileRab.rab$w_rsz = sizeof(AUTH_HTAREC);

      /* if reached end-of-file then add a record, else update zeroed one */
      if (status == RMS$_EOF)
         status = sys$put (&AuthFileRab, 0, 0);
      else
         status = sys$update (&AuthFileRab, 0, 0);
      if (VMSok (status))
      {
         /* put or update completed successfully */
         if (!LeaveFileOpen) sys$close (&AuthFileFab, 0, 0);
         return (SS$_NORMAL);
      }
      /* RMS error */
      sys$close (&AuthFileFab, 0, 0);
      return (status);
   }

   /* sanity check error, should never get here */
   return (SS$_BUGCHECK);
}

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

