/*****************************************************************************
/*
                                 HTAdmin.c

Command-line administration of the WASD .$HTA authentication databases.

   $ HTADMIN <database> [<username>] [<qualifiers>]


USAGE EXAMPLES
--------------
To create a new database named EXAMPLE.$HTA (in the current directory)

   $ HTADMIN EXAMPLE /CREATE

Delete an existing database

   $ HTADMIN EXAMPLE /DELETE /CONFIRM

List (briefly) the records

   $ HTADMIN EXAMPLE

List (briefly) the specific user record DANIEL

   $ HTADMIN EXAMPLE DANIEL

List all detail (132 colums) of the specified user record

   $ HTADMIN EXAMPLE DANIEL /FULL

To add the new record DANIEL with default read access

   $ HTADMIN EXAMPLE DANIEL /ADD /NAME="Mark Daniel" 

Add the new record DANIEL with contact details and read+write access

   $ HTADMIN EXAMPLE DANIEL /ADD /WRITE /CONTACT="Postal Address"

Add the new record DANIEL and be prompted for a password, or to specify the
password on the command-line, or have the utility generate a password or
four-digit PIN style password (which is displayed after the record is
sucessfully added)

   $ HTADMIN EXAMPLE DANIEL /ADD /NAME="Mark Daniel" /PASSWORD
   $ HTADMIN EXAMPLE DANIEL /ADD /NAME="Mark Daniel" /PASSWORD=cher10s
   $ HTADMIN EXAMPLE DANIEL /ADD /NAME="Mark Daniel" /GENERATE
   $ HTADMIN EXAMPLE DANIEL /ADD /NAME="Mark Daniel" /PIN

To update an existing record

   $ HTADMIN EXAMPLE DANIEL /UPDATE /EMAIL="Mark.Daniel@wasd.vsm.com.au"

Update the specified record's password (interactively) then to generate a four
digit PIN for a password (which is then displayed)

   $ HTADMIN EXAMPLE DANIEL /UPDATE /PASSWORD
   $ HTADMIN EXAMPLE DANIEL /UPDATE /GENERATE
   $ HTADMIN EXAMPLE DANIEL /UPDATE /PIN

Disable then enable an existing user record without changing anything else

   $ HTADMIN EXAMPLE DANIEL /UPDATE /DISABLE
   $ HTADMIN EXAMPLE DANIEL /UPDATE /ENABLE

To list the entire database, first briefly, then in 132 column mode (with all
detail), then finally as a comma-separated listing

   $ HTADMIN EXAMPLE
   $ HTADMIN EXAMPLE /FULL
   $ HTADMIN EXAMPLE /CSV


SORT DETAILS
------------
The /SORT qualifier sorts the current database records according to the /SORT=
parameters.  It can be used with the /LIST qualifier to produce ordered reports
or will output the records into another authentication file.  By default it
sorts ascending by username.  Qualifier parameters allow a sort by DATE or
COUNT.  Each of these allows the further specification of which date or count;
ACCESS, CHANGE or FAILURE.

Generating a listing with specified order

  $ HTADMIN EXAMPLE /LIST /SORT=DATE=ACCESS
  $ HTADMIN EXAMPLE /LIST /SORT=COUNT=FAILURE /OUTPUT=EXAMPLE.LIS

Sort descending by username into a higher version of EXAMPLE.$HTA

  $ HTADMIN EXAMPLE /SORT

To sort by username into another .$HTA file

  $ HTADMIN EXAMPLE /SORT /OUTPUT=ANOTHER

List by most-recently accessed

  $ HTADMIN EXAMPLE /LIST /SORT=DATE

List by most-recently failed to authenticate

  $ HTADMIN EXAMPLE /LIST /SORT=DATE=FAILURE

Sort file into order by most frequently authenticated (accessed)

  $ HTADMIN EXAMPLE /SORT=COUNT


QUALIFIERS
----------
/ADD                    add a new record
/CONFIRM                confirm deletion of database
/CONTACT="<string>"     contact information for record
/CREATE                 create a new database
/CSV[=TAB|char]         comma-separated listing (optional character)
/DATABASE=              database name (or as command-line parameter)
/DELETE                 delete a database or username record from a database
/DISABLED               username record is disabled (cannot be used)
/EMAIL="<string>"       email address for record
/ENABLED                username record is enabled (can be used)
/FULL                   listing showing full details
/GENERATE               generate a six character password
/HELP                   brief usage information
/[NO]HTTPS              synonym for /SSL
/LIST                   listing (brief by default, see /FULL and /CSV)
/MODIFY                 synonym for /UPDATE
/NAME="<string>"        full name for username record
/OUTPUT=                alternate output for database listing
/PASSWORD[=<string>]    username record password (prompts if not supplied)
/PIN                    generate four-digit "PIN number" for password
/[NO]READ               username can/can't read
/SORT[=<parameters>]    sort the records into a new/nuther database
/[NO]SSL                only allowed to authenticate when using SSL (https:)
/[NO]WRITE              username can/can't write
/UPDATE                 update an existing username record
/USER=<string>          username
/VERSION                display version of HTADMIN


BUILD DETAILS
-------------
See BUILD_HTADMIN.COM procedure.


COPYRIGHT
---------
Copyright (C) 2003-2020 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 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
16-APR-2016  MGD  v1.0.4, .AddedBinTime to .AddedTime64 name change
26-MAY-2008  MGD  v1.0.3, bugfix; /READ and /WRITE set-reset flags for
                          HTTP methods in-line with [SRC.HTTPD]HTADMIN.C
21-MAY-2008  MGD  v1.0.2, bugfix; GetSetPassword() string descriptors
                          bugfix; upper-case password string regardless
23-DEC-2003  MGD  v1.0.1, minor conditional mods to support IA64
23-SEP-2003  MGD  v1.0.0, initial
*/
/*****************************************************************************/

#define SOFTWAREVN "1.0.4"
#define SOFTWARENM "HTADMIN"
#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

char  Copyright [] = 
"Copyright (C) 2003-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 any later version.\n\
http://www.gnu.org/licenses/gpl.txt\n";

#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 <climsgdef.h>
#include <descrip.h>
#include <sordef.h>
#include <sor$routines.h>
#include <ssdef.h>
#include <stsdef.h>
#include <uaidef.h>

/* application related header files */
#include "[-.httpd]wasd.h"

BOOL  AddUserName,
      CommandConfirmed,
      CreateDatabase,
      DeleteDatabase,
      DeleteUserName,
      GeneratePassword,
      GeneratePIN,
      ListBrief,
      ListCsv,
      ListDatabase,
      ListFull,
      FlagDisabled,
      FlagEnabled,
      FlagRead,
      FlagNoRead,
      FlagWrite,
      FlagNoWrite,
      FlagSslOnly,
      FlagNoSslOnly,
      RequiresWrite,
      SortDatabase,
      SortDate,
      SortAccess,
      SortChange,
      SortCount,
      SortFailure,
      SortUserName,
      UpdateUserName;

int  ContactLength,
     EmailLength,
     FullNameLength,
     PaswordLength,
     UserNameLength;

unsigned long  Time64 [2];

char  CsvChar;

char  *ContactPtr,
      *DatabaseNamePtr,
      *EmailPtr,
      *FullNamePtr,
      *OutputPtr,
      *PasswordPtr,
      *UserNamePtr;

char  ExpFileName [256],
      ResFileName [256];

char  Utility [] = "HTADMIN";

AUTH_HTAREC  HtaRecord;

struct FAB  DatabaseFab;
struct NAM  DatabaseNam;
struct RAB  DatabaseRab;
struct XABPRO  DatabaseXabPro;

/* prototypes */
int DatabaseAddRecord ();
DtabaseCreate (char*, BOOL);
DatabaseDelete ();
int DatabaseDeleteRecord ();
int DatabaseOpen ();
DatabasePrintRecord (int);
DatabaseRecordPut ();
DatabaseSort ();
int DatabaseUpdateRecord ();
GetParameters ();
int GetSetPassword ();
BOOL strsame (char*, char*, int);

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

main ()

{
   char  *cptr;

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

   GetParameters ();

   if (UserNamePtr)
   {
      UserNameLength = strlen(UserNamePtr);
      if (UserNameLength > sizeof(HtaRecord.UserName))
      {
         fprintf (stdout, "%%%s-E-USERNAME, too long\n", Utility);
         exit (SS$_RESULTOVF | STS$M_INHIB_MSG);
      }
      for (cptr = UserNamePtr; *cptr; cptr++)
         if (!isalnum(*cptr) && *cptr != '_' && *cptr != '-') break;
      if (*cptr)
      {
         fprintf (stdout, "%%%s-E-USERNAME, not acceptable\n", Utility);
         exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
      }
   }

   if (FullNamePtr)
   {
      FullNameLength = strlen(FullNamePtr);
      if (FullNameLength > sizeof(HtaRecord.FullName))
      {
         fprintf (stdout, "%%%s-E-FULLNAME, too long\n", Utility);
         exit (SS$_RESULTOVF | STS$M_INHIB_MSG);
      }
   }

   if (ContactPtr)
   {
      ContactLength = strlen(ContactPtr);
      if (ContactLength > sizeof(HtaRecord.Contact))
      {
         fprintf (stdout, "%%%s-E-CONTACT, too long\n", Utility);
         exit (SS$_RESULTOVF | STS$M_INHIB_MSG);
      }
   }

   if (EmailPtr)
   {
      EmailLength = strlen(EmailPtr);
      if (EmailLength > sizeof(HtaRecord.Email))
      {
         fprintf (stdout, "%%%s-E-EMAIL, too long\n", Utility);
         exit (SS$_RESULTOVF | STS$M_INHIB_MSG);
      }
   }

   if (CreateDatabase)
   {
      if (!DatabaseNamePtr || !DatabaseNamePtr[0]) exit (CLI$_INSFPRM);
      DatabaseCreate (DatabaseNamePtr, false);
      exit (SS$_NORMAL);
   }

   if (DeleteDatabase && !UserNamePtr) DatabaseDelete ();

   sys$gettim (&Time64);

   if (ListDatabase)
      DatabaseSort ();
   else
   if (SortDatabase)
      DatabaseSort ();
   else
   if (AddUserName)
      DatabaseAddRecord ();
   else
   if (DeleteUserName)
      DatabaseDeleteRecord ();
   else
   if (UpdateUserName)
      DatabaseUpdateRecord ();
   else
   {
      ListDatabase = true;
      if (UserNamePtr) ListFull = true;
      DatabaseSort ();
   }
}

/*****************************************************************************/
/*
Add a new record.  First make sure it does not already exist.  The check for
a previously deleted (zeroed) record.  If found update the record with the new
content.  If not found add a new record to the end of the file.
*/

int DatabaseAddRecord ()

{
   BOOL  ReuseEmptyRecord;
   int  status;
   char  *cptr, *sptr, *zptr;

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

   if (!UserNameLength)
   {
      fprintf (stdout, "%%%s-E-USERNAME, not specified\n", Utility);
      exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
   }

   DatabaseOpen (true);

   status = DatabaseFindRecord ();
   if (status == SS$_NORMAL) exit (RMS$_REX);
   if (status != RMS$_EOF) exit (status);

   /* now look for a previously deleted (empty) record */
   status = sys$rewind (&DatabaseRab, 0, 0);
   while (VMSok (status = sys$get (&DatabaseRab, 0, 0)))
      if (!HtaRecord.UserNameLength) break;
   if (VMSnok(status) && status != RMS$_EOF) exit (status);
   if (status == RMS$_EOF)
      ReuseEmptyRecord = false;
   else
      ReuseEmptyRecord = true;

   memset (&HtaRecord, 0, sizeof(HtaRecord));

   zptr = (sptr = HtaRecord.UserName) + sizeof(HtaRecord.UserName);
   for (cptr = UserNamePtr; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   HtaRecord.UserNameLength = sptr - HtaRecord.UserName;

   if (FullNamePtr)
   {
      zptr = (sptr = HtaRecord.FullName) + sizeof(HtaRecord.FullName)-1;
      for (cptr = FullNamePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
   }
   if (ContactPtr)
   {
      zptr = (sptr = HtaRecord.Contact) + sizeof(HtaRecord.Contact)-1;
      for (cptr = ContactPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
   }
   if (EmailPtr)
   {
      zptr = (sptr = HtaRecord.Email) + sizeof(HtaRecord.Email)-1;
      for (cptr = EmailPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
   }

   /* unless otherwise specified newly created usernames have read access */
   if (FlagRead || !FlagNoRead)
      HtaRecord.Flags |= (AUTH_FLAG_GET | AUTH_FLAG_HEAD);
   if (FlagNoRead) HtaRecord.Flags &= ~(AUTH_FLAG_GET | AUTH_FLAG_HEAD);
   if (FlagWrite) HtaRecord.Flags |= (AUTH_FLAG_CONNECT | AUTH_FLAG_DELETE |
                                      AUTH_FLAG_POST | AUTH_FLAG_PUT);
   if (FlagNoWrite) HtaRecord.Flags &= ~(AUTH_FLAG_CONNECT | AUTH_FLAG_DELETE |
                                         AUTH_FLAG_POST | AUTH_FLAG_PUT);
   /* unless otherwise specified newly create usernames are enabled */
   if (!FlagDisabled) HtaRecord.Flags |= AUTH_FLAG_ENABLED;
   if (FlagDisabled) HtaRecord.Flags &= ~AUTH_FLAG_ENABLED;
   if (FlagSslOnly) HtaRecord.Flags |= AUTH_FLAG_HTTPS_ONLY;
   if (FlagNoSslOnly) HtaRecord.Flags &= ~AUTH_FLAG_HTTPS_ONLY;

   if (PasswordPtr)
      GetSetPassword ();
   else
   if (GeneratePassword)
      GetSetPassword ();
   else
   if (GeneratePIN)
      GetSetPassword ();
   else
      /* not specified, make the password unusable */
      memset (&HtaRecord.HashedPwd, 0xaa, sizeof(HtaRecord.HashedPwd));

   memcpy (&HtaRecord.AddedTime64, &Time64, 8);

   DatabaseRab.rab$l_rbf = &HtaRecord;
   DatabaseRab.rab$w_rsz = sizeof(HtaRecord);

   if (ReuseEmptyRecord)
      status = sys$update (&DatabaseRab, 0, 0);
   else
      status = sys$put (&DatabaseRab, 0, 0);

   if (VMSok(status))
   {
      if (GeneratePassword)
         fprintf (stdout, "%%%s-I-PASSWORD, for %s is \"%s\"\n",
                  Utility, UserNamePtr, PasswordPtr);
      else
      if (GeneratePIN)
         fprintf (stdout, "%%%s-I-PIN, for %s is %s\n",
                  Utility, UserNamePtr, PasswordPtr);
   }

   exit (status);
}

/*****************************************************************************/
/*
Modify the specified field(s) and then update the current record.
*/

int DatabaseUpdateRecord ()

{
   BOOL  UpdateRecord;
   int  status;
   char  *cptr, *sptr, *zptr;

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

   if (!UserNameLength)
   {
      fprintf (stdout, "%%%s-E-USERNAME, not specified\n", Utility);
      exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
   }

   DatabaseOpen (true);

   status = DatabaseFindRecord ();
   if (status == RMS$_EOF) exit (RMS$_RNF);
   if (VMSnok (status)) exit (status);

   UpdateRecord = false;
   if (FullNamePtr)
   {
      zptr = (sptr = HtaRecord.FullName) + sizeof(HtaRecord.FullName)-1;
      for (cptr = FullNamePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      UpdateRecord = true;
   }
   if (ContactPtr)
   {
      zptr = (sptr = HtaRecord.Contact) + sizeof(HtaRecord.Contact)-1;
      for (cptr = ContactPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      UpdateRecord = true;
   }
   if (EmailPtr)
   {
      zptr = (sptr = HtaRecord.Email) + sizeof(HtaRecord.Email)-1;
      for (cptr = EmailPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      UpdateRecord = true;
   }
   if (FlagRead)
   {
      HtaRecord.Flags |= (AUTH_FLAG_GET | AUTH_FLAG_HEAD);
      UpdateRecord = true;
   }
   if (FlagNoRead)
   {
      HtaRecord.Flags &= ~(AUTH_FLAG_GET | AUTH_FLAG_HEAD);
      UpdateRecord = true;
   }
   if (FlagWrite)
   {
      HtaRecord.Flags |= (AUTH_FLAG_CONNECT | AUTH_FLAG_DELETE |
                          AUTH_FLAG_POST | AUTH_FLAG_PUT);
      UpdateRecord = true;
   }
   if (FlagNoWrite)
   {
      HtaRecord.Flags &= ~(AUTH_FLAG_CONNECT | AUTH_FLAG_DELETE |
                           AUTH_FLAG_POST | AUTH_FLAG_PUT);
      UpdateRecord = true;
   }
   if (FlagSslOnly)
   {
      HtaRecord.Flags |= AUTH_FLAG_HTTPS_ONLY;
      UpdateRecord = true;
   }
   if (FlagNoSslOnly)
   {
      HtaRecord.Flags &= ~AUTH_FLAG_HTTPS_ONLY;
      UpdateRecord = true;
   }
   if (FlagEnabled)
   {
      HtaRecord.Flags |= AUTH_FLAG_ENABLED;
      UpdateRecord = true;
   }
   if (FlagDisabled)
   {
      HtaRecord.Flags &= ~AUTH_FLAG_ENABLED;
      UpdateRecord = true;
   }

   if (PasswordPtr)
   {
      GetSetPassword ();
      UpdateRecord = true;
   }
   else
   if (GeneratePassword)
   {
      GetSetPassword ();
      UpdateRecord = true;
   }
   else
   if (GeneratePIN)
   {
      GetSetPassword ();
      UpdateRecord = true;
   }

   if (!UpdateRecord) exit (SS$_NORMAL);

   memcpy (&HtaRecord.LastChangeTime64, &Time64, 8);
   HtaRecord.ChangeCount++;

   DatabaseRab.rab$l_rbf = &HtaRecord;
   DatabaseRab.rab$w_rsz = sizeof(HtaRecord);

   status = sys$update (&DatabaseRab, 0, 0);

   if (VMSok(status))
   {
      if (GeneratePassword)
         fprintf (stdout, "%%%s-I-PASSWORD, for %s is \"%s\"\n",
                  Utility, UserNamePtr, PasswordPtr);
      else
      if (GeneratePIN)
         fprintf (stdout, "%%%s-I-PIN, for %s is %s\n",
                  Utility, UserNamePtr, PasswordPtr);
   }

   exit (status);
}

/*****************************************************************************/
/*
Delete the by zeroing the content and updating the current record.
*/

int DatabaseDeleteRecord ()

{
   int  status;

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

   if (!UserNameLength)
   {
      fprintf (stdout, "%%%s-E-USERNAME, not specified\n", Utility);
      exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
   }

   DatabaseOpen (true);

   status = DatabaseFindRecord ();
   if (status == RMS$_EOF) exit (RMS$_RNF);
   if (VMSnok (status)) exit (status);

   memset (&HtaRecord, 0, sizeof(HtaRecord));

   status = sys$update (&DatabaseRab, 0, 0);

   exit (status);
}

/*****************************************************************************/
/*
Sort the records (according to the /SORT= parameters) into a new version of the
same database file name, or into /OUTPUT= specification, or into record print.
*/

int DatabaseSort ()

{
   static $DESCRIPTOR (DateFaoDsc, "!20%D\0");

   int  status,
        InputRecordCount,
        OutputRecordCount,
        SortLrl,
        SortOptions;
   unsigned short  ShortLength;
   char  *cptr;
   char  DateAsAt [32];
   $DESCRIPTOR (DateAsAtDsc, DateAsAt);
   $DESCRIPTOR (RecordDsc, "");

   typedef struct
   {
      unsigned short  type;
      unsigned short  order;
      unsigned short  offset;
      unsigned short  len;
   } KEY_INFO;

   struct
   {
      unsigned short  num;
      KEY_INFO  key [1];
   } SortKeys;

   globalvalue int  SOR$M_STABLE;

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

   RecordDsc.dsc$a_pointer = &HtaRecord;
   RecordDsc.dsc$w_length = sizeof(HtaRecord);

   if (!SortUserName && !SortDate && !SortCount) SortUserName = true;

   if (SortUserName)
   {
      SortKeys.key[0].type = DSC$K_DTYPE_T;
      SortKeys.key[0].order = 0;
      SortKeys.key[0].offset = 0;
      SortKeys.key[0].len = AUTH_MAX_HTA_USERNAME_LENGTH;
      SortKeys.num = 1;
   }
   else
   if (SortDate)
   {
      if (SortAccess)
         cptr = (char*)&HtaRecord.LastAccessTime64;
      else
      if (SortChange)
         cptr = (char*)&HtaRecord.LastChangeTime64;
      else
      if (SortFailure)
         cptr = (char*)&HtaRecord.LastFailureTime64;
      else
         exit (SS$_BUGCHECK);

      SortKeys.key[0].type = DSC$K_DTYPE_Q;
      SortKeys.key[0].order = 1;
      SortKeys.key[0].offset = cptr - (char*)&HtaRecord;
      SortKeys.key[0].len = 8;
      SortKeys.num = 1;
   }
   else
   if (SortCount)
   {
      if (SortAccess)
         cptr = (char*)&HtaRecord.AccessCount;
      else
      if (SortChange)
         cptr = (char*)&HtaRecord.ChangeCount;
      else
      if (SortFailure)
         cptr = (char*)&HtaRecord.FailureCount;
      else
         exit (SS$_BUGCHECK);

      SortKeys.key[0].type = DSC$K_DTYPE_LU;
      SortKeys.key[0].order = 0;
      SortKeys.key[0].offset = cptr - (char*)&HtaRecord;
      SortKeys.key[0].len = 4;
      SortKeys.num = 1;
   }
   else
      exit (SS$_BUGCHECK);

   SortOptions = SOR$M_STABLE;
   SortLrl = sizeof(HtaRecord);

   status = sor$begin_sort (&SortKeys, &SortLrl, &SortOptions,
                            0, 0, 0, 0, 0, 0);
   if (VMSnok (status)) exit (status);

   DatabaseOpen (false);

   InputRecordCount = OutputRecordCount = 0;

   while (VMSok (status = sys$get (&DatabaseRab, 0, 0)))
   {
      /* check the version of the authorization database */
      if (HtaRecord.DatabaseVersion &&
          HtaRecord.DatabaseVersion != AUTH_HTA_VERSION)
         exit (SS$_INCOMPAT);

      if (!HtaRecord.UserNameLength) continue;

      /* seems a bit heavy-handed for a single record but simplifies code */
      if (UserNamePtr)
         if (!strsame (UserNamePtr, HtaRecord.UserName, -1))
            continue;

      status = sor$release_rec (&RecordDsc, 0);
      if (VMSnok (status)) exit (status);
      InputRecordCount++;
   }

   sys$close (&DatabaseFab, 0, 0);

   if (ListDatabase)
   {
      if (OutputPtr && OutputPtr[0])
         if (!(stdout = freopen (OutputPtr, "w", stdout)))
            exit (vaxc$errno);

      if (!ListCsv && !ListFull) ListBrief = true;

      if (ListFull)
      {
         sys$fao (&DateFaoDsc, NULL, &DateAsAtDsc, 0);
         fprintf (stdout,
"\n\
     %s as at %s\n\
     %-*s %-*s %-10s %s\n\
     %-20s  %-29s  %-29s  %-29s\n\
-----------------------------------------------------------\
-----------------------------------------------------------\n\
",
                  ExpFileName, DateAsAt,
                  AUTH_MAX_HTA_USERNAME_LENGTH,
                  "User Name",
                  AUTH_MAX_FULLNAME_LENGTH,
                  "Full Name",
                  "Access",
                  "Status",
                  "Added",
                  "Modified/Last",
                  "Accessed/Last",
                  "Failed/Last");
      }
   }

   if (InputRecordCount)
   {
      status = sor$sort_merge (0);
      if (VMSnok (status)) exit (status);

      if (ListDatabase)
      {
         for (;;)
         {
            status = sor$return_rec (&RecordDsc, &ShortLength, 0);
            if (status == SS$_ENDOFFILE) break;
            if (VMSnok (status)) exit (status);
            DatabasePrintRecord (++OutputRecordCount);
         }
      }
      else
      {
         if (OutputPtr && OutputPtr[0])
            DatabaseCreate (OutputPtr, true);
         else
            DatabaseCreate (DatabaseNamePtr, true);

         /* construct a RAB and connect to the just-created database file */
         DatabaseRab = cc$rms_rab;
         DatabaseRab.rab$l_fab = &DatabaseFab;
         DatabaseRab.rab$l_rbf = &HtaRecord;
         DatabaseRab.rab$w_rsz = sizeof(HtaRecord);

         status = sys$connect (&DatabaseRab, 0, 0);
         if (VMSnok (status)) exit (status);

         for (;;)
         {
            status = sor$return_rec (&RecordDsc, &ShortLength, 0);
            if (status == SS$_ENDOFFILE) break;
            if (VMSnok (status)) exit (status);
            OutputRecordCount++;
            status = sys$put (&DatabaseRab, 0, 0);
            if (VMSnok (status)) exit (status);
         }

         sys$close (&DatabaseFab, 0, 0);
      }
   }

   if (ListDatabase)
   {
      if (ListFull)
         fputs ("\
----------------------------------------------------------\
-----------------------------------------------------------\n\
\n",
                stdout);
   }

   status = sor$end_sort (0);
   if (VMSnok (status)) exit (status);

   if (InputRecordCount != OutputRecordCount) exit (SS$_BUGCHECK);
}

/*****************************************************************************/
/*
Print a single record.
*/

DatabasePrintRecord (int RecordCount)

{
   static $DESCRIPTOR (DateFaoDsc, "!20%D\0");

   int  status;
   char  *cptr,
         *AccessPtr,
         *NonePtr;
   char  DateAdded [32],
         DateLastAccess [32],
         DateLastChange [32],
         DateLastFailure [32];
   $DESCRIPTOR (DateAddedDsc, DateAdded);
   $DESCRIPTOR (DateLastAccessDsc, DateLastAccess);
   $DESCRIPTOR (DateLastChangeDsc, DateLastChange);
   $DESCRIPTOR (DateLastFailureDsc, DateLastFailure);

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

   if (ListCsv) NonePtr = ""; else NonePtr = "(none)";

   if (HtaRecord.AddedTime64[0] || HtaRecord.AddedTime64[1])
   {
      sys$fao (&DateFaoDsc, NULL, &DateAddedDsc,
               &HtaRecord.AddedTime64);
      if (DateAdded[0] == ' ') DateAdded[0] = '0';
   }
   else
      strcpy (DateAdded, NonePtr);

   if (HtaRecord.LastChangeTime64[0] || HtaRecord.LastChangeTime64[1])
   {
      sys$fao (&DateFaoDsc, NULL, &DateLastChangeDsc,
               &HtaRecord.LastChangeTime64);
      if (DateLastChange[0] == ' ') DateLastChange[0] = '0';
   }
   else
      strcpy (DateLastChange, NonePtr);

   if (HtaRecord.LastAccessTime64[0] || HtaRecord.LastAccessTime64[1])
   {
      sys$fao (&DateFaoDsc, NULL, &DateLastAccessDsc,
               &HtaRecord.LastAccessTime64);
      if (DateLastAccess[0] == ' ') DateLastAccess[0] = '0';
   }
   else
      strcpy (DateLastAccess, NonePtr);

   if (HtaRecord.LastFailureTime64[0] || HtaRecord.LastFailureTime64[1])
   {
      sys$fao (&DateFaoDsc, NULL, &DateLastFailureDsc,
               &HtaRecord.LastFailureTime64);
      if (DateLastFailure[0] == ' ') DateLastFailure[0] = '0';
   }
   else
      strcpy (DateLastFailure, NonePtr);

   if (HtaRecord.Flags & AUTH_FLAG_GET &&
       HtaRecord.Flags & AUTH_FLAG_POST)
      AccessPtr = "read+write";
   else
   if (HtaRecord.Flags & AUTH_FLAG_GET)
      AccessPtr = "read";
   else
   if (HtaRecord.Flags & AUTH_FLAG_POST)
      AccessPtr = "write";
   else
      AccessPtr = "none";

   if (ListBrief)
      fprintf (stdout, "%04.04d %-*s %-*s %-10s %s\n",
               RecordCount,
               AUTH_MAX_HTA_USERNAME_LENGTH,
               HtaRecord.UserName,
               AUTH_MAX_FULLNAME_LENGTH,
               HtaRecord.FullName,
               AccessPtr,
               HtaRecord.Flags & AUTH_FLAG_ENABLED ?
                  "enabled" : "disabled");
   else
   if (ListFull)
   {
      fprintf (stdout, "%04.04d %-*s %-*s %-10s %s%s\n",
               RecordCount,
               AUTH_MAX_HTA_USERNAME_LENGTH,
               HtaRecord.UserName,
               AUTH_MAX_FULLNAME_LENGTH,
               HtaRecord.FullName,
               AccessPtr,
               HtaRecord.Flags & AUTH_FLAG_ENABLED ?
                  "enabled" : "disabled",
               HtaRecord.Flags & AUTH_FLAG_HTTPS_ONLY ?
                  " SSL-only" : "");
      if (HtaRecord.Contact[0])
      {
         /* massage contact newlines into TABs */
         for (cptr = HtaRecord.Contact; *cptr; cptr++)
         {
            if (*cptr == '\r') *cptr = ' ';
            if (*cptr == '\n') *cptr = '\t';
         }
         fprintf (stdout, "     %s\n", HtaRecord.Contact);
      }
      if (HtaRecord.Email[0])
         fprintf (stdout, "     %s\n", HtaRecord.Email);
      if (!HtaRecord.Contact[0] && !HtaRecord.Email[0])
         fprintf (stdout, "     (no contact or email)\n");
      fprintf (stdout, "     %-20s  %7d  %-20s  %7d  %-20s  %7d  %-20s\n",
               DateAdded, HtaRecord.ChangeCount, DateLastChange,
                          HtaRecord.AccessCount, DateLastAccess,
                          HtaRecord.FailureCount, DateLastFailure);
   }
   else
   if (ListCsv)
   {
      if (!isprint(CsvChar) && CsvChar != '\t') CsvChar = ',';
      for (cptr = HtaRecord.FullName; *cptr; cptr++)
         if (*cptr == CsvChar) *cptr = ' ';
      /* massage contact newlines into TABs */
      for (cptr = HtaRecord.Contact; *cptr; cptr++)
      {
         if (*cptr == '\r') *cptr = ' ';
         if (*cptr == '\n') *cptr = '\t';
         if (*cptr == CsvChar) *cptr = ' ';
      }
      for (cptr = HtaRecord.Email; *cptr; cptr++)
         if (*cptr == CsvChar) *cptr = ' ';
      fprintf (stdout,
"%d%c%s%c%s%c%s%c%s%c%s%c%s%c%s%c%s%c%d%c%s%c%d%c%s%c%d%c%s\n",
            RecordCount,
            CsvChar,
               HtaRecord.UserName,
               CsvChar,
               HtaRecord.FullName,
               CsvChar,
               HtaRecord.Contact,
               CsvChar,
               HtaRecord.Email,
               CsvChar,
               AccessPtr,
               CsvChar,
               HtaRecord.Flags & AUTH_FLAG_ENABLED ?
                  "enabled" : "disabled",
               CsvChar,
               HtaRecord.Flags & AUTH_FLAG_HTTPS_ONLY ?
                  "SSL-only" : "",
               CsvChar,
               DateAdded,
               CsvChar,
               HtaRecord.ChangeCount,
               CsvChar,
               DateLastChange,
               CsvChar,
               HtaRecord.AccessCount,
               CsvChar,
               DateLastAccess,
               CsvChar,
               HtaRecord.FailureCount,
               CsvChar,
               DateLastFailure);
   }
}

/*****************************************************************************/
/*
Search from start to finish for the CLI-specified username,  Leaves record
positioned for update and returns normal when found, EOF if not found.
*/

int DatabaseFindRecord ()

{
   int  status;

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

   if (!UserNameLength) exit (SS$_BUGCHECK);

   while (VMSok (status = sys$get (&DatabaseRab, 0, 0)))
   {
      /* check the version of the authorization database */
      if (HtaRecord.DatabaseVersion &&
          HtaRecord.DatabaseVersion != AUTH_HTA_VERSION)
         exit (SS$_INCOMPAT);
      if (HtaRecord.UserNameLength != UserNameLength) continue;
      if (strsame (UserNamePtr, HtaRecord.UserName, -1)) return (SS$_NORMAL);
   }

   return (status);
}

/*****************************************************************************/
/*
Open and connect to the database file for either read or write.
*/

int DatabaseOpen (BOOL ForWrite)

{
   int  status;

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

   if (!DatabaseNamePtr || !DatabaseNamePtr[0]) exit (CLI$_INSFPRM);

   DatabaseFab = cc$rms_fab;
   DatabaseFab.fab$l_fna = DatabaseNamePtr;  
   DatabaseFab.fab$b_fns = strlen(DatabaseNamePtr);
   DatabaseFab.fab$l_dna = HTA_FILE_TYPE;  
   DatabaseFab.fab$b_dns = strlen(HTA_FILE_TYPE);
   DatabaseFab.fab$l_nam = &DatabaseNam;

   if (ForWrite)
      DatabaseFab.fab$b_fac = FAB$M_GET | FAB$M_PUT | FAB$M_UPD;
   else
      DatabaseFab.fab$b_fac = FAB$M_GET;
   DatabaseFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT | FAB$M_SHRUPD;

   DatabaseNam = cc$rms_nam;
   DatabaseNam.nam$l_esa = ExpFileName;
   DatabaseNam.nam$b_ess = sizeof(ExpFileName)-1;

   status = sys$open (&DatabaseFab, 0, 0);
   if (VMSnok (status)) exit (status);

   DatabaseRab = cc$rms_rab;
   DatabaseRab.rab$l_fab = &DatabaseFab;
   /* 2 buffers of sixty-four blocks (records) each */
   DatabaseRab.rab$b_mbc = 64;
   DatabaseRab.rab$b_mbf = 2;
   /* read ahead performance option, read regardless of lock */
   DatabaseRab.rab$l_rop = RAB$M_RAH | RAB$M_RRL;

   DatabaseRab.rab$l_ubf = &HtaRecord;
   DatabaseRab.rab$w_usz = sizeof(HtaRecord);

   status = sys$connect (&DatabaseRab, 0, 0);
   if (VMSnok (status)) exit (status);

   return (status);
}

/*****************************************************************************/
/*
Create the specified database.  If 'NewVersion' is false then check if the file
exists andreport the error if it does.  If true the a later version will be
created if it already exists.
*/

DatabaseCreate
(
char *DatabaseName,
BOOL NewVersion
)
{
   int  status;

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

   DatabaseFab = cc$rms_fab;
   DatabaseFab.fab$l_fna = DatabaseName;  
   DatabaseFab.fab$b_fns = strlen(DatabaseName);
   DatabaseFab.fab$l_dna = HTA_FILE_TYPE;  
   DatabaseFab.fab$b_dns = strlen(HTA_FILE_TYPE);
   DatabaseFab.fab$l_nam = &DatabaseNam;

   DatabaseNam = cc$rms_nam;
   DatabaseNam.nam$l_esa = ExpFileName;
   DatabaseNam.nam$b_ess = sizeof(ExpFileName)-1;

   status = sys$parse (&DatabaseFab, 0, 0);
   if (VMSnok (status)) exit (status);

   if (!NewVersion)
   {
      status = sys$search (&DatabaseFab, 0, 0);
      if (VMSnok (status) && status != RMS$_FNF) exit (status);
      if (status != RMS$_FNF) exit (RMS$_FEX);
   }

   /* OK, now carefully adjust some of the data in the RMS structures */
   DatabaseFab.fab$b_fac = FAB$M_GET | FAB$M_PUT | FAB$M_UPD;
   DatabaseFab.fab$l_fop = FAB$M_SQO;
   DatabaseFab.fab$w_mrs = sizeof(HtaRecord);
   DatabaseFab.fab$b_rfm = FAB$C_FIX;
   DatabaseFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT | FAB$M_SHRUPD;
   DatabaseFab.fab$l_xab = &DatabaseXabPro;

   DatabaseXabPro = cc$rms_xabpro;
   DatabaseXabPro.xab$l_nxt = 0;
   /* w:,g:,o:rwed,s:rwed */
   DatabaseXabPro.xab$w_pro = 0xff00;

   status = sys$create (&DatabaseFab, 0, 0);
   if (VMSnok (status)) exit (status);
}

/*****************************************************************************/
/*
Delete the specified database.
*/

DatabaseDelete ()

{
   int  cnt, status;

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

   if (!DatabaseNamePtr || !DatabaseNamePtr[0]) exit (CLI$_INSFPRM);
   if (!CommandConfirmed)
   {
      fprintf (stdout,
"%%%s-E-DELETE, confirm database deletion using /CONFIRM\n",
               Utility);
      exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
   }

   DatabaseFab = cc$rms_fab;
   DatabaseFab.fab$l_fna = DatabaseNamePtr;  
   DatabaseFab.fab$b_fns = strlen(DatabaseNamePtr);
   DatabaseFab.fab$l_dna = HTA_FILE_TYPE;  
   DatabaseFab.fab$b_dns = strlen(HTA_FILE_TYPE);
   DatabaseFab.fab$l_nam = &DatabaseNam;

   DatabaseNam = cc$rms_nam;
   DatabaseNam.nam$l_esa = ExpFileName;
   DatabaseNam.nam$b_ess = sizeof(ExpFileName)-1;

   status = sys$parse (&DatabaseFab, 0, 0);
   if (VMSnok (status)) exit (status);

   status = sys$search (&DatabaseFab, 0, 0);
   if (VMSnok (status)) exit (status);

   cnt = 0;
   while (VMSok (status = sys$erase (&DatabaseFab, 0, 0))) cnt++;
   if (status == RMS$_FNF && cnt) status = SS$_NORMAL;

   exit (status);
}

/*****************************************************************************/
/*
If a /PASSWORD=<string> has not been supplied at the CLI then prompt for a
non-echoed password and confirmation.  Hash the password string into the
record's quadword hashed password.
*/

int GetSetPassword ()

{
   static char  Passwd1 [64],
                Passwd2 [64];

   int  cnt, status;
   char  *cptr, *sptr;
   MD5_HASH  Md5Hash;
   $DESCRIPTOR (PasswordDsc, "");
   $DESCRIPTOR (UserNameDsc, "");

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

   if (GeneratePassword)
   {
      /* generate a six alpha-numeric character password */
      Md5Digest (&Time64, sizeof(Md5Hash), &Md5Hash);
      cptr = (char*)&Md5Hash;
      sptr = PasswordPtr = Passwd1;
      cnt = 0;
      while (cnt < 6)
      {
         if (cptr > (char*)&Md5Hash + sizeof(Md5Hash))
         {
            Md5Digest (&Md5Hash, sizeof(Md5Hash), &Md5Hash);
            cptr = (char*)&Md5Hash;
         }
         *cptr = tolower(*cptr);
         if (cnt == 5)
         {
            /* digit required */
            if (*cptr >= '0' && *cptr <= '9')
            {
               *sptr++ = *cptr;
               cnt++;
            }
         }
         else
         if (*cptr == 'a' || *cptr == 'e' || *cptr == 'i' ||
             *cptr == 'o' || *cptr == 'u' || *cptr == 'y') 
         {
            if (cnt % 2)
            {
               /* vowel required */
               *sptr++ = *cptr;
               cnt++;
            }
         }
         else
         if (*cptr >= 'a' && *cptr <= 'z' &&
             *cptr != 'a' && *cptr != 'e' && *cptr != 'i' &&
             *cptr != 'o' && *cptr != 'u' && *cptr != 'y') 
         {
            if (!(cnt % 2))
            {
               /* consonant required */
               *sptr++ = *cptr;
               cnt++;
            }
         }
         cptr++;
      }
      *sptr = '\0';
   }
   else
   if (GeneratePIN)
   {
      /* generate a four-digit PIN  */
      sprintf (PasswordPtr = Passwd1, "%04.04d",
               Time64[0] / 1000 % 10000);
   }
   else
   if (!PasswordPtr || !PasswordPtr[0])
   {
      /* read password from user (without echo) */
      Passwd1[0] = Passwd2[0] = '\0';
      stdin = freopen ("SYS$INPUT", "r", stdin,
                       "ctx=rec", "rop=rne", "rop=tmo", "tmo=30");
      if (!stdin) exit (vaxc$errno);

      fprintf (stdout, "Enter password []: ");
      fgets (Passwd1, sizeof(Passwd1), stdin);
      fputc ('\n', stdout);
      Passwd1 [sizeof(Passwd1)-1] = '\0';
      if (!Passwd1[0]) exit (RMS$_TMO);
      if (Passwd1[0]) Passwd1 [strlen(Passwd1)-1] = '\0';
      if (!Passwd1[0]) exit (SS$_NORMAL);

      fprintf (stdout, "Confirm password []: ");
      fgets (Passwd2, sizeof(Passwd2), stdin);
      fputc ('\n', stdout);
      if (!Passwd2[0]) exit (RMS$_TMO);
      Passwd2 [sizeof(Passwd2)-1] = '\0';
      if (Passwd2[0]) Passwd2 [strlen(Passwd2)-1] = '\0';
      if (!Passwd2[0]) exit (SS$_NORMAL);

      if (!strsame (Passwd1, Passwd2, -1))
      {
         fprintf (stdout, "%%%s-E-PASSWORD, not confirmed\n", Utility);
         exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
      }
      PasswordPtr = Passwd1;
   }

   if (!(isdigit(PasswordPtr[0]) &&
         isdigit(PasswordPtr[1]) &&
         isdigit(PasswordPtr[2]) &&
         isdigit(PasswordPtr[3]) &&
         !PasswordPtr[4]) &&
       strlen(PasswordPtr) < AUTH_MIN_PASSWORD)
   {
      /* not a four-digit PIN and less than the required characters */
      fprintf (stdout, "%%%s-E-PASSWORD, too short\n", Utility);
      exit (CLI$_INSFPRM | STS$M_INHIB_MSG);
   }

   /* force password to upper-case */
   for (cptr = PasswordPtr; *cptr; cptr++) *cptr = toupper(*cptr);

   PasswordDsc.dsc$a_pointer = PasswordPtr;
   PasswordDsc.dsc$w_length = strlen(PasswordPtr);
   UserNameDsc.dsc$a_pointer = UserNamePtr;
   UserNameDsc.dsc$w_length = strlen(UserNamePtr);

   memset (&HtaRecord.HashedPwd, 0, sizeof(HtaRecord.HashedPwd));

   status = sys$hash_password (&PasswordDsc, UAI$C_PURDY_S, 0,
                               &UserNameDsc, &HtaRecord.HashedPwd);

   if (VMSnok (status)) exit (status);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

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

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

   if (!(clptr = getenv ("HTADMIN$PARAM")))
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

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

      *clptr = ch;
      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 (!*aptr) continue;

      if (strsame (aptr, "/ADD", 4))
      {
         AddUserName = true;
         continue;
      }
      if (strsame (aptr, "/CONFIRM", 5))
      {
         CommandConfirmed = true;
         continue;
      }
      if (strsame (aptr, "/CONTACT=", 5))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ContactPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CREATE", 4))
      {
         CreateDatabase = true;
         continue;
      }
      if (strsame (aptr, "/CSV=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         ListCsv = true;
         if (strsame (cptr, "TAB", -1))
            CsvChar = '\t';
         else
            CsvChar = *cptr;
         continue;
      }
      if (strsame (aptr, "/DATABASE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         DatabaseNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DELETE", 4))
      {
         DeleteUserName = DeleteDatabase = true;
         continue;
      }
      if (strsame (aptr, "/DISABLED", 4))
      {
         FlagDisabled = true;
         continue;
      }
      if (strsame (aptr, "/EMAIL=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         EmailPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/ENABLED", 4))
      {
         FlagEnabled = true;
         continue;
      }
      if (strsame (aptr, "/FULL", 4))
      {
         ListFull = true;
         continue;
      }
      if (strsame (aptr, "/GENERATE", 4))
      {
         GeneratePassword = true;
         continue;
      }
      if (strsame (aptr, "/HELP", 4))
      {
         ShowHelp ();
         exit (SS$_NORMAL);
      }
      if (strsame (aptr, "/HTTPS", 4))
      {
         FlagSslOnly = true;
         continue;
      }
      if (strsame (aptr, "/NOHTTPS", 6))
      {
         FlagNoSslOnly = true;
         continue;
      }
      if (strsame (aptr, "/LIST", 4))
      {
         ListDatabase = true;
         continue;
      }
      if (strsame (aptr, "/MODIFY", 4))
      {
         UpdateUserName = true;
         continue;
      }
      if (strsame (aptr, "/NAME=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         FullNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/OUTPUT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         OutputPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/PASSWORD=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         PasswordPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/PIN", 4))
      {
         GeneratePIN = true;
         continue;
      }
      if (strsame (aptr, "/READ", 4))
      {
         FlagRead = true;
         continue;
      }
      if (strsame (aptr, "/NOREAD", 6))
      {
         FlagNoRead = true;
         continue;
      }
      if (strsame (aptr, "/SORT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         SortDatabase = true;

         if (strsame (cptr, "COUNT", 4))
            SortCount = true;
         else
         if (strsame (cptr, "DATE", 4))
            SortDate = true;
         else
         if (strsame (cptr, "USERNAME", 4))
         {
            SortUserName = true;
            continue;
         }
         else
         if (*cptr)
         {
            fprintf (stdout, "%%%s-E-IVKEYW, unrecognized keyword\n \\%s\\\n",
                        Utility, cptr);
            exit (CLI$_IVKEYW | STS$M_INHIB_MSG);
         }
         else
            SortUserName = true;

         while (*cptr && *cptr != '=') cptr++;
         if (*cptr) cptr++;

         if (strsame (cptr, "ACCESS", 5))
            SortAccess = true;
         else
         if (strsame (cptr, "CHANGE", 5) ||
             strsame (cptr, "MODIFY", 5))
            SortChange = true;
         else
         if (strsame (cptr, "FAILURE", 5))
            SortFailure = true;
         else
         if (*cptr)
         {
            fprintf (stdout, "%%%s-E-IVKEYW, unrecognized keyword\n \\%s\\\n",
                        Utility, cptr);
            exit (CLI$_IVKEYW | STS$M_INHIB_MSG);
         }
         else
            SortAccess = true;
         continue;
      }
      if (strsame (aptr, "/SSL", 4))
      {
         FlagSslOnly = true;
         continue;
      }
      if (strsame (aptr, "/NOSSL", 6))
      {
         FlagNoSslOnly = true;
         continue;
      }
      if (strsame (aptr, "/WRITE", 4))
      {
         FlagWrite = true;
         continue;
      }
      if (strsame (aptr, "/NOWRITE", 6))
      {
         FlagNoWrite = true;
         continue;
      }
      if (strsame (aptr, "/UPDATE", 4))
      {
         UpdateUserName = true;
         continue;
      }
      if (strsame (aptr, "/USER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         /* force username to upper-case */
         for (UserNamePtr = cptr; *cptr; cptr++) *cptr = toupper(*cptr);
         continue;
      }
      if (strsame (aptr, "/VERSION", 4))
      {
         fprintf (stdout, "%%%s-I-VERSION, %s\n%s",
                  Utility, SOFTWAREID, Copyright);
         exit (SS$_NORMAL);
      }

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

      if (!DatabaseNamePtr)
      {
         DatabaseNamePtr = aptr;
         for (cptr = aptr; *cptr; cptr++) *cptr = toupper(*cptr);
         continue;
      }

      if (!UserNamePtr)
      {
         UserNamePtr = aptr;
         for (cptr = aptr; *cptr; cptr++) *cptr = toupper(*cptr);
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (CLI$_MAXPARM | 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
)
{
   /*********/
   /* begin */
   /*********/

   while (*sptr1 && *sptr2)
   {
      if (tolower(*sptr1++) != tolower(*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}
 
/*****************************************************************************/
/*
*/

int ShowHelp ()

{
   fprintf (stdout,
"Usage for HTADMIN Utility (%s)\n\
\n\
$ HTADMIN <database> [<username>] [<qualifiers>]\n\
\n\
Allows command-line administration of WASD .$HTA authorization databases.\n\
\n\
/ADD /CONFIRM /CONTACT=\"<string>\" /CREATE /CSV[=TAB|char] /DATABASE=<name>\n\
/DELETE /DISABLED /EMAIL=\"<string>\" /ENABLED /FULL /GENERATE /HELP /[NO]HTTPS\n\
/LIST /MODIFY /NAME=\"<string>\" /OUTPUT=<filename> /PASSWORD[=<string>] /PIN\n\
/[NO]READ /SORT[=<parameter>] /[NO]SSL /[NO]WRITE /UPDATE /USER=<name> /VERSION\n\
\n\
$ HTADMIN EXAMPLE          !brief list of the EXAMPLE database records\n\
$ HTADMIN EXAMPLE /FULL    !full (132 column) listing\n\
$ HTADMIN EXAMPLE /CSV     !comma-separated value listing\n\
$ HTADMIN EXAMPLE DANIEL   !full listing of record DANIEL\n\
$ HTADMIN EXAMPLE DANIEL /ADD /NAME=\"Mark Daniel\"   !add a new record\n\
$ HTADMIN EXAMPLE DANIEL /UPDATE /EMAIL=\"Mark.Daniel@vsm.com.au\"\n\
$ HTADMIN EXAMPLE DANIEL /UPDATE /READ /WRITE       !change user access\n\
$ HTADMIN EXAMPLE DANIEL /UPDATE /PASSWORD          !prompts for password\n\
$ HTADMIN EXAMPLE DANIEL /DELETE                    !delete the record\n\
$ HTADMIN EXAMPLE /CREATE                           !create a new database\n\
$ HTADMIN EXAMPLE /DELETE /CONFIRM                  !delete the database\n\
\n",
            SOFTWAREID);

   return (SS$_NORMAL);
}

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

