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


    THE GNU GENERAL PUBLIC LICENSE APPLIES DOUBLY TO ANYTHING TO DO WITH
                    AUTHENTICATION AND AUTHORIZATION!

    This package is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; version 2 of the License, or any later
    version.

>   This package is distributed in the hope that it will be useful,
>   but WITHOUT ANY WARRANTY; without even the implied warranty of
>   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
>   GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

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
---------------
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_V6
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 60000000
#undef __CRTL_VER
#define __CRTL_VER 60000000
#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 in VAX C 3.2 <uaidef.h> */
#define UAI$M_RESTRICTED 0x8
#define UAI$C_PURDY_S 3

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

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern int  ServerPort;

extern unsigned long  SysPrvMask[];

extern char  ErrorSanityCheck[],
             ServerHostName[],
             ServerHostPort[],
             SoftwareID[],
             Utility[];

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];
   char  *cptr, *sptr, *zptr;
   char  A1HexDigest [33],
         HexDigest [33];
   AUTH_HTAREC AuthHtRecord;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, 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++ = toupper(*cptr++));
   *sptr = '\0';
   UserNameDsc.dsc$w_length = sptr - UserName;

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

      /* close the database */
      AuthAccessHtDatabase (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++ = toupper(*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 */
               AuthAccessHtDatabase (NULL, false, NULL, NULL,
                                     NULL, NULL, NULL);
               rqptr->rqResponse.ErrorTextPtr = MsgFor(rqptr,MSG_AUTH_USER);
               ErrorVmsStatus (rqptr, status, FI_LI);
               return (status);
            }

            if (WATCHING(rqptr) &&
                WATCH_MODULE(WATCH_MOD_AUTH))
                WatchThis (rqptr, FI_LI, 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 (!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 (!strcmp (rqptr->rqAuth.DigestResponsePtr, HexDigest))
                  PasswordAuthenticated = true;
            }
         }

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

         if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
             WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AUTHENTICATED");
      }

      if (!AuthenticatePassword || PasswordAuthenticated)
      {
         /* set the appropriate authorization flags */
         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;
      }
   }

   if (UserNameEnabled && (!AuthenticatePassword || PasswordAuthenticated))
   {
      AuthHtRecord.AccessCount++;
      memcpy (&AuthHtRecord.LastAccessBinTime, &rqptr->rqTime.Vms64bit, 8);
   }
   else
   {
      AuthHtRecord.FailureCount++;
      memcpy (&AuthHtRecord.LastFailureBinTime, &rqptr->rqTime.Vms64bit, 8);
   }

   /* update the record, close the database file */
   status = AuthAccessHtDatabase (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 AuthAccessHtDatabase
(
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 (NULL, FI_LI, WATCH_MOD_AUTH, 
                 "AuthAccessHtDatabase() !&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 (Debug) fprintf (stdout, "sys$update() %%X%08.08X\n", status);
      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);
      }
      if (Debug) fprintf (stdout, "sys$rewind() %%X%08.08X\n", status);
   }
   else
   {
      /**********************************/
      /* open currently closed database */
      /**********************************/

      if (rqptr->rqAuth.PathParameterPtr &&
          rqptr->rqAuth.PathParameterPtr[0])
      {
         cptr = rqptr->rqAuth.PathParameterPtr;
         if (strsame (cptr, "/DIRECTORY=", 11)) cptr += 11;
      }
      else
      if (rqptr->rqAuth.DirectoryPtr)
         cptr = rqptr->rqAuth.DirectoryPtr;
      else
      if (rqptr->ConfigDirectory[0])
         cptr = rqptr->ConfigDirectory;
      else
         cptr = HTA_DIRECTORY;

      /* 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_CATEGORY(WATCH_AUTH))
         WatchThis (rqptr, FI_LI, 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 (Debug)
         fprintf (stdout, "AuthHtRecordPtr->UserName |%s|\n",
                  AuthHtRecordPtr->UserName);
**/
      if (UserNameLength != AuthHtRecordPtr->UserNameLength) continue;
      cptr = AuthHtRecordPtr->UserName;
      sptr = UserName;
      while (*cptr && *sptr && tolower(*cptr) == tolower(*sptr))
         { cptr++; sptr++; }
      if (*cptr || *sptr) continue;
      break;
   }
   if (Debug) fprintf (stdout, "sys$get() %%X%08.08X\n", status);

   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 (Debug) fprintf (stdout, "sys$update() %%X%08.08X\n", status);
      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 (Debug) fprintf (stdout, "sys$put/update() %%X%08.08X\n", status);
      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);
}

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

