/***************************************************************************** /* HTAdmin.c Command-line administration of the WASD .$HTA authentication databases. $ HTADMIN [] [] 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="" 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="" 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="" full name for username record /OUTPUT= alternate output for database listing /PASSWORD[=] username record password (prompts if not supplied) /PIN generate four-digit "PIN number" for password /[NO]READ username can/can't read /SORT[=] 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= username /VERSION display version of HTADMIN BUILD DETAILS ------------- See BUILD_HTADMIN.COM procedure. COPYRIGHT --------- Copyright (C) 2003-2021 Mark G.Daniel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. VERSION HISTORY (update SOFTWAREVN as well!) --------------- 18-DEC-2020 MGD v1.0.5, VAX no longer implemented 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.5" #define SOFTWARENM "HTADMIN" #ifdef __ALPHA # define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN #endif #ifdef __ia64 # define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN #endif #ifdef __VAX # error VAX no longer implemented #endif #ifdef __x86_64 # define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN #endif char Copyright [] = "Copyright (C) 2003-2021 Mark G.Daniel\n\ Licensed under the Apache License, Version 2.0 (the \"License\");\n\ you may not use this file except in compliance with the License.\n\ You may obtain a copy of the License at\n\ http://www.apache.org/licenses/LICENSE-2.0\n\ Unless required by applicable law or agreed to in writing, software\n\ distributed under the License is distributed on an \"AS IS\" BASIS,\n\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\ See the License for the specific language governing permissions and\n\ limitations under the License.\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 #include #include #include /* VMS related header files */ #include #include #include #include #include #include #include /* 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= 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 [] []\n\ \n\ Allows command-line administration of WASD .$HTA authorization databases.\n\ \n\ /ADD /CONFIRM /CONTACT=\"\" /CREATE /CSV[=TAB|char] /DATABASE=\n\ /DELETE /DISABLED /EMAIL=\"\" /ENABLED /FULL /GENERATE /HELP /[NO]HTTPS\n\ /LIST /MODIFY /NAME=\"\" /OUTPUT= /PASSWORD[=] /PIN\n\ /[NO]READ /SORT[=] /[NO]SSL /[NO]WRITE /UPDATE /USER= /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); } /****************************************************************************/