/*****************************************************************************/
/*
                                Control.c

This module implements the HTTPd command-line, and the distributed, to-all
server processes (from command-line or Admin Menu), control functionality.

Command-line server control commands:

  o  AUTH          reload authorization file
  o  AUTH=LOAD     reload authorization file
  o  AUTH=PURGE    purge authentication records from cache
  o  AUTH=SKELKEY=       see AUTH.C for syntax and usage
  o  CACHE=ON      turn file caching on
  o  CACHE=OFF     turn file caching off
  o  CACHE=PURGE         purge all data cached
  o  DCL=DELETE    unconditionally delete all DCL script processes, busy or not
  o  DCL=PURGE     delete idle script processes, mark busy for later deletion
  o  DECNET=PURGE        disconnect idle DECnet script tasks
  o  DECNET=DISCONNECT   forceably disconnect all DECnet script tasks
  o  EXIT          exit after all client activity complete (nothing new started)
  o  EXIT=NOW      exit right now regardless of connections
  o  INSTANCE=MAX|CPU|integer    explicitly set the startup instance value
  o  LIST          *special-case* when used with /ALL just list all servers
  o  LOG=OPEN      open the log file(s)
  o *LOG=OPEN=name       *OBSOLETE* open the log file using the specified name
  o  LOG=REOPEN          closes then reopens the log
  o *LOG=REOPEN=name     *OBSOLETE* log reopened using specified name
  o  LOG=CLOSE     close the log file(s)
  o  LOG=FLUSH     flush the log file(s)
  o *LOG=FORMAT=string   *OBSOLETE* set the log format
  o *LOG=PERIOD=string   *OBSOLETE* set the log period
  o  MAP           reload mapping rule file
  o  PROXY=ON                proxy processing enabled
  o  PROXY=OFF               proxy processing disabled
  o  PROXY=PURGE=BACKGROUND  background cache purge
  o  PROXY=PURGE=REACTIVE    reactive cache purge
  o  PROXY=PURGE=ROUTINE     routine cache purge
  o  PROXY=PURGE=HOST        host name cache purge
  o  PROXY=STATISTICS        perform statistics scan of cache
  o  PROXY=STOP=SCAN         stop an in-progress purge or statistics scan
  o  RESTART[=integer]       effectively exit server image then re-activate
  o  RESTART=NOW[=integer]   restart NOW regardless of connections
  o  RESTART=QUIET[=integer] restart if-and-when current requests reach zero
  o  SSL=CA=LOAD   reload the CA verification file
  o *SSL=KEY=PASSWORD        *special case* prompt/supply private key password
  o  THROTTLE=RELEASE        release all queued requests for processing
  o  THROTTLE=TERMINATE      terminate all queued requests 
  o  THROTTLE=ZERO           zero throttle statistics
  o  ZERO          zero accounting

These commands are entered at the DCL command line (interpreted in the CLI.c
module) in the following manner.

  $ HTTPD /DO=command

If /ALL is added then the command is applied to all HTTPd server processes on
the node or cluster (asuming there is more than one executing, requires
SYSLCK).  Due to architectural constraints, those commands marked "*" in the
above list cannot be used with the /ALL qualifier.                    

  $ HTTPD /DO=command /ALL

For example:

  $ HTTPD /DO=EXIT           the server exits if when clients connected
  $ HTTPD /DO=EXIT=NOW       the server exists immediately
  $ HTTPD /DO=RESTART        server exits and then restarts
  $ HTTPD /DO=RESTART/ALL    all servers on node/cluster exit and restarts
  $ HTTPD /DO=DCL=PURGE      delete all persistent DCL script processes
  $ HTTPD /DO=LOG=CLOSE      close the log file
  $ HTTPD /DO=ZERO/ALL       zero the accounting on all servers
  $ HTTPD /DO=LIST/ALL       *special-case*, just list all servers

Single node and cluster directives are implemented using cluster-wide locking
resource names and shared memory.  For single server /DO= control the
lock-resource name serves to notify (all) server(s) that a directive has been
written into the global section shared memory control buffer.  All
participating servers check this area, only the one with a current directive
written into its shared-memory control buffer performs it.  For multiple server
/DO=/ALL control, the lock value block contains the actual directive and so
participating servers act on that alone.  The originating image requests a
cluster-wide lock before originating a directive, effectively locking others
out during the process (which is most commonly a very short period).

Servers could potentially be grouped using the resource names.  The /ALL
qualifier accepts an optional string that will become part of the lock resource
name.  Hence when servers belonging to a particular group are started up the
startup procedure would include something like "/ALL=1", and then Admin Menu
directives would apply to only those in that group, and command-line directives
would be used "/DO=command/ALL=1", etc.  Note that the /ALL= parameter can be
any alphanumeric string up to 8 characters.


SERVER CONTROL LOCK
-------------------
When a server starts up it uses InstanceLockNotifySet() to enqueue a CR
(concurrent read) lock against the resource name using ControlHttpdLock(). 
This lock allows a "blocking" AST to be delivered (back to ControlHttpdLock())
indicating a CLI-command or other server is wishing to initiate a control
action.  It releases that original CR lock then immediately enqueues another CR
so that it can read the lock value block subsequently written to by the
initiator's now-granted EX mode lock (see immediately below).  The lock value
block will contain a maximum 15 character, null-terminated string (the 16
bytes) with the directive.  The AST delivery will provide this value block to
ControlHttpdAst() which calls the appropriate function to perform or ignore the
directive.


INITIATOR CONTROL LOCK
----------------------
To initiate some control activity on one or more servers the initiator (either
at the command-line or from the Admin Menu) enqueues a CR (concurrent read)
lock on the appropriate resource name.  It then converts that lock to EX
(exclusive).  Those with original CR locks enqueued have a "blocking" AST
delivered, release their locks allowing the initiator to obtain the requested
EX lock.  It then, using a sys$deq(), writes the command string into the 16
byte lock value block.  When the exclusive lock is dequeued the servers waiting
on the subsequent CR locks have them delivered, allowing them to read the value
block and they can then check the lock status block for a directive (which
they may or may not act upon depending on the contents).


VERSION HISTORY
---------------
31-DEC-2003  MGD  ControlDelay() if multiple instances add a further, random
                  delay to prevent mass suicide with reduced instances and a
                  restart=now directive
06-JAN-2003  MGD  bugfix; ControlEnqueueCommand() occasional race condition
                  between InstanceLockList() and InstanceLockNotifyNow() AST
                  disabling SYSLCK during list (happy birthday Naomi)
07-DEC-2002  MGD  skeleton-key authentication
30-MAY-2002  MGD  RESTART=QUIET restart when(-and-if) requests reach zero
21-MAY-2002  MGD  ControlZeroAccounting() under instance supervisor control
04-APR-2002  MGD  proxy maintenance STOP scan
29-SEP-2001  MGD  significant changes in support of per-node instances
04-AUG-2001  MGD  support module WATCHing
30-JUN-2001  MGD  bugfix; missing log open code, strlen(.._AS),
                  obsolete "log=open=" and "log=reopen="
18-MAY-2001  MGD  global section shared memory and DLM for control,
                  provide SSL private key password directive
06-APR-2001  MGD  allow WATCH to note control events
13-MAR-2001  MGD  add THROTTLE=...
28-DEC-2000  MGD  add SSL=CA=LOAD
03-DEC-2000  MGD  bugfix; CachePurge()
18-JUN-2000  MGD  add node/cluster-wide, sys$enq()-driven directives (/ALL)
12-JAN-2000  MGD  rework control reporting, adding OPCOM messages
20-JAN-1999  MGD  proxy serving controls
15-AUG-1998  MGD  decnet=purge, decnet=disconnect
16-MAR-1998  MGD  restart=now
05-OCT-1997  MGD  cache and logging period control
28-SEP-1997  MGD  reinstated some convenience commands (AUTH, DCL, and ZERO)
01-FEB-1997  MGD  HTTPd version 4 (removed much of its previous functionality)
01-OCT-1996  MGD  minor changes to authorization do commands
01-JUL-1996  MGD  controls for path/realm-based authorization/authentication
01-DEC-1995  MGD  HTTPd version 3
12-APR-1995  MGD  support logging
03-APR-1995  MGD  support authorization
20-DEC-1994  MGD  initial development for multi-threaded version of HTTPd
*/
/*****************************************************************************/

#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 <jpidef.h>
#include <lckdef.h>
#include <lkidef.h>
#include <prvdef.h>
#include <psldef.h>
#include <secdef.h>
#include <ssdef.h>
#include <stsdef.h>

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

#define WASD_MODULE "CONTROL"

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

BOOL  ControlDoAllHttpd,
      ControlExitRequested,
      ControlRestartQuiet,
      ControlRestartRequested;

unsigned long  ControlPid;

char  *ControlCommandPtr;

char  ControlBuffer [256],
      ControlNodeName [16],
      ControlProcessName [16],
      ControlUserName [13];
      
/********************/
/* external storage */
/********************/

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

extern BOOL  CacheEnabled,
             HttpdServerExecuting,
             HttpdTicking,
             InstanceNodeSupervisor,
             OperateWithSysPrv,
             ProxyCacheFreeSpaceAvailable,
             ProxyServingEnabled;

extern int  CliServerPort,
            EfnWait,
            ExitStatus,
            HttpdGblSecPages,
            HttpdGblSecVersion,
            HttpdTickSecond,
            InstanceConnectCurrent,
            InstanceGroupNumber,
            InstanceNodeConfig,
            InstanceNodeCurrent,
            OpcomMessages,
            ServerPort;

extern unsigned short  HttpdNumTime[];

extern unsigned long  GblSecPrvMask[],
                      SysLckMask[],
                      SysPrvMask[],
                      WorldMask[];

extern char  ErrorSanityCheck[],
             LoggingFileName[],
             LoggingFormatUser[],
             LoggingPeriodName[],
             SoftwareID[],
             Utility[];

extern INSTANCE_LOCK  InstanceLockTable[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern HTTPD_GBLSEC  *HttpdGblSecPtr;
extern PROXY_ACCOUNTING_STRUCT  *ProxyAccountingPtr;
extern WATCH_STRUCT  Watch;

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

ControlInit ()

{
   int  status;

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER, "ControlInit()");

   InstanceLockNotifySet (INSTANCE_NODE_DO, &ControlHttpdAst);
   InstanceLockNotifySet (INSTANCE_CLUSTER_DO, &ControlHttpdAst);
}

/*****************************************************************************/
/*
This is the primary function called when a /DO= is made at the command line. 
This function attempts to map the appropriate global section to access memory
shared with the server.  It then call the appropriate function to request the
server perform the directive, either locally or if /ALL was used on the CLI
across all servers in the group (cluster).
*/ 

int ControlCommand (char *Command)

{
   /* system global section, map into first available virtual address */
   static int MapFlags = SEC$M_SYSGBL | SEC$M_EXPREG | SEC$M_WRT;
   /* it is recommended to map into any virtual address in the region (P0) */
   static unsigned long  InAddr [2] = { 0x200, 0x200 };

   int  cnt, status,
        LockIndex,
        ByteSize,
        PageSize,
        PeriodMinutes;
   unsigned short  ShortLength;
   char  *cptr, *sptr, *zptr;
   char  GblSecName [32],
         PrivateKeyPasswd [64];
   $DESCRIPTOR (GblSecNameDsc, GblSecName);
   unsigned long  RetAddr [2];

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER, "ControlCommand()");

   if (CliServerPort) ServerPort = CliServerPort;
   if (ServerPort < 1 || ServerPort > 65535)
   {
      WriteFaoStdout ("%!AZ-E-PORT, IP port out-of-range\n", Utility);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }

   WriteFao (GblSecName, sizeof(GblSecName), &ShortLength,
             GBLSEC_NAME_FAO, HTTPD_NAME, HTTPD_GBLSEC_VERSION,
             InstanceGroupNumber, "HTTPD");
   GblSecNameDsc.dsc$w_length = ShortLength;

   /* map the specified global section */
   sys$setprv (1, &GblSecPrvMask, 0, 0);
   status = sys$mgblsc (&InAddr, &RetAddr, 0, MapFlags, &GblSecNameDsc, 0, 0);
   sys$setprv (0, &GblSecPrvMask, 0, 0);

   if (VMSok (status))
   {
      ByteSize = (RetAddr[1]+1) - RetAddr[0];
      PageSize = (RetAddr[1]+1) - RetAddr[0] >> 9;
      HttpdGblSecPtr = (HTTPD_GBLSEC*)RetAddr[0];
      HttpdGblSecPages = PageSize;
   }
   else
   {
      if (status == SS$_NOSUCHSEC)
      {
         WriteFaoStdout (
"%!AZ-E-SERVER, no such server!!\n\
-SYSTEM-E-NOSUCHSEC, no such (global) section\n",
            Utility);
         exit (status | STS$M_INHIB_MSG);
      }
      else
         exit (status);
   }

   if (HttpdGblSecPtr->GblSecVersion != HttpdGblSecVersion)
      WriteFaoStdout (
"%!AZ-W-GBLSECMIS, global section version mismatch (!8XL/!8XL)\n\
-!AZ-W-TRYING, will attempt directive (no guarantees!!)\n",
         Utility, HttpdGblSecPtr->GblSecVersion, HttpdGblSecVersion,
         Utility);

   if (strsame (Command, CONTROL_SSL_PKPASSWD, -1))
   {
      /************************/
      /* private key password */
      /************************/

      stdin = freopen ("SYS$INPUT", "r", stdin,
                       "ctx=rec", "rop=rne", "rop=tmo", "tmo=60");
      if (!stdin) exit (vaxc$errno);
      memset (PrivateKeyPasswd, 0, sizeof(PrivateKeyPasswd));
      fprintf (stdout, "Enter private key password []: ");
      fgets (PrivateKeyPasswd, sizeof(PrivateKeyPasswd), stdin);
      fputc ('\n', stdout);
      /* ensure it's null terminated */
      PrivateKeyPasswd [sizeof(PrivateKeyPasswd)-1] = '\0';
      /* remove any trailing newline */
      if (PrivateKeyPasswd[0])
         PrivateKeyPasswd [strlen(PrivateKeyPasswd)-1] = '\0';

      /* enable SYSPRV to allow writing into the global section */
      sys$setprv (1, &SysPrvMask, 0, 0);

      zptr = (sptr = HttpdGblSecPtr->PkPasswd) +
             sizeof(HttpdGblSecPtr->PkPasswd)-1;
      for (cptr = PrivateKeyPasswd; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';

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

   if (strsame (Command, CONTROL_AUTH_SKELETON,
                sizeof(CONTROL_AUTH_SKELETON)-1))
   {
      /*******************************/
      /* skeleton key authentication */
      /*******************************/

      InstanceMutexLock (INSTANCE_MUTEX_HTTPD);

      PeriodMinutes = -1;
      cptr = Command + sizeof(CONTROL_AUTH_SKELETON)-1;
      if (!*cptr || *cptr == '0')
         PeriodMinutes = 0;
      else
      {
         zptr = (sptr = HttpdGblSecPtr->AuthSkelKeyUserName) +
                sizeof(HttpdGblSecPtr->AuthSkelKeyUserName)-1;
         while (*cptr && *cptr != ':' && sptr < zptr) *sptr++ = *cptr++;
         *sptr = '\0';
         if (*cptr == ':')
         {
            cptr++;
            zptr = (sptr = HttpdGblSecPtr->AuthSkelKeyPassword) +
                   sizeof(HttpdGblSecPtr->AuthSkelKeyPassword)-1;
            while (*cptr && *cptr != ':' && sptr < zptr) *sptr++ = *cptr++;
            *sptr = '\0';
            if (HttpdGblSecPtr->AuthSkelKeyUserName[0] != '_')
               WriteFaoStdout ("%!AZ-E-DO, _<username>:<password> \n", Utility);
            else
            if (strlen(HttpdGblSecPtr->AuthSkelKeyUserName) < 7)
               WriteFaoStdout ("%!AZ-E-DO, username too short\n", Utility);
            else
            if (strsame (HttpdGblSecPtr->AuthSkelKeyUserName, "_SKELE", 6))
               WriteFaoStdout ("%!AZ-E-DO, not a good choice!!\n", Utility);
            else
            if (sptr - HttpdGblSecPtr->AuthSkelKeyPassword < 8)
               WriteFaoStdout ("%!AZ-E-DO, password too short\n", Utility);
            else
            {
               if (strsame (HttpdGblSecPtr->AuthSkelKeyUserName,
                            HttpdGblSecPtr->AuthSkelKeyPassword, -1) ||
                   strsame (HttpdGblSecPtr->AuthSkelKeyUserName+1,
                            HttpdGblSecPtr->AuthSkelKeyPassword, -1))
               {
                  WriteFaoStdout ("%!AZ-E-DO, not a good choice!!\n", Utility);
                  PeriodMinutes = -1;
               }
               else
               if (*cptr == ':')
               {
                  cptr++;
                  PeriodMinutes = atoi(cptr);
                  /* less than a minute or greater than a week */
                  if (PeriodMinutes <= 0 || PeriodMinutes > 10080)
                  {
                     WriteFaoStdout ("%!AZ-E-DO, invalid period\n", Utility);
                     PeriodMinutes = -1;
                  }
               }
               else
                  PeriodMinutes = 60;
            }
         }
         else
            WriteFaoStdout ("%!AZ-E-DO, _<username>:<password>\n", Utility);
      }

      if (PeriodMinutes <= 0)
      {
         memset (HttpdGblSecPtr->AuthSkelKeyUserName, 0,
                 sizeof(HttpdGblSecPtr->AuthSkelKeyUserName));
         memset (HttpdGblSecPtr->AuthSkelKeyPassword, 0,
                 sizeof(HttpdGblSecPtr->AuthSkelKeyPassword));
         HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond = 0;
      }
      else
      {
         /* ControlHttpdAst() will turn this into seconds into the future */
         HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond = PeriodMinutes;
      }

      InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);

      /* if an error then don't enqueue the command */
      if (PeriodMinutes < 0) exit (SS$_NORMAL);

      /* mask the sensitive information */
      cptr = Command + sizeof(CONTROL_AUTH_SKELETON)-1;
      if (*cptr != '0') *(unsigned short*)cptr = '*\0';
      /* drop through to enqueue the command */
   }       

   if (ControlDoAllHttpd)
   {
      LockIndex = INSTANCE_CLUSTER_DO;
      sptr = "ALL";
   }
   else
   {
      LockIndex = INSTANCE_NODE_DO;
      sptr = "";
   }
   status = InstanceLockControl ();
   if (status == SS$_NOTQUEUED)
   {
      WriteFaoStdout ("%!AZ-E-DO!AZ, in use elsewhere!!\n", Utility, sptr);
      exit (status | STS$M_INHIB_MSG);
   }
   if (VMSnok (status)) exit (status);
   cptr = ControlEnqueueCommand (LockIndex, Command);
   for (cnt = CONTROL_RESPONSE_SECONDS * 5; cnt; cnt--)
   {
      /* poll the progress of the enqueue */
      if (!InstanceLockNotifyNow (0, NULL)) break;
      ControlSleep (200);
   }
   InstanceUnLockControl ();
   if (!cnt)
   {
      WriteFaoStdout (
"%!AZ-E-DO!AZ, command (en)queueing did not complete within !UL seconds\n",
                      Utility, sptr, CONTROL_RESPONSE_SECONDS);
      return (SS$_TIMEOUT | STS$M_INHIB_MSG);
   }

   if (*cptr == '!')
      WriteFaoStdout ("%!AZ-E-DO!AZ, !AZ\n", Utility, sptr, cptr+1);
   else
      WriteFaoStdout ("%!AZ-I-DO!AZ, !AZ\n", Utility, sptr, cptr);
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Check that the command is known and can be done via a /ALL or via the
ADMINISTRATION MENU. Enqueues a CR (concurrent read) lock on the specified
resource name, then gets the number of current locks (i.e. other processes)
currently with an interest in that lock.  Then converts that lock to EX
(exclusive) and using a sys$deq() writes the command string into the 16 byte
lock value block.  When the exclusive lock is removed the other processes
waiting on CR locks get them, reading the value block and excuting the command
therein.
*/

char* ControlEnqueueCommand
(
int LockIndex,
char *Command
)
{
   static char  String [512];

   int  status,
        ServerCount;
   char  *cptr;

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                 "ControlEnqueueCommand() !UL !&Z", LockIndex, Command);

   /* a synonym (because I always forget the correct command) */
   if (strsame (Command, CONTROL_PROXY_PURGE_STOP, -1))
      strcpy (Command, CONTROL_PROXY_STOP_SCAN);

   if (strsame (Command, CONTROL_AUTH_LOAD1, -1) ||
       strsame (Command, CONTROL_AUTH_LOAD2, -1) ||
       strsame (Command, CONTROL_AUTH_PURGE, -1) ||
       strsame (Command, CONTROL_AUTH_SKELETON,
                sizeof(CONTROL_AUTH_SKELETON)-1) ||
       strsame (Command, CONTROL_CACHE_ON, -1) ||
       strsame (Command, CONTROL_CACHE_OFF, -1) ||
       strsame (Command, CONTROL_CACHE_PURGE, -1) ||
       strsame (Command, CONTROL_DCL_DELETE, -1) ||
       strsame (Command, CONTROL_DCL_PURGE, -1) ||
       strsame (Command, CONTROL_DECNET_PURGE, -1) ||
       strsame (Command, CONTROL_DECNET_DISCONNECT, -1) ||
       strsame (Command, CONTROL_EXIT, -1) ||
       strsame (Command, CONTROL_EXIT_NOW, -1) ||
       strsame (Command, CONTROL_INSTANCE, sizeof(CONTROL_INSTANCE)-1) ||
       strsame (Command, CONTROL_LIST, -1) ||
       strsame (Command, CONTROL_LOG_OPEN, -1) ||
       strsame (Command, CONTROL_LOG_CLOSE, -1) ||
       strsame (Command, CONTROL_LOG_FLUSH, -1) ||
       strsame (Command, CONTROL_LOG_REOPEN, -1) ||
       strsame (Command, CONTROL_MAP, -1) ||
       strsame (Command, CONTROL_PROXY_ON, -1) ||
       strsame (Command, CONTROL_PROXY_OFF, -1) ||
       strsame (Command, CONTROL_PROXY_PURGE_BCKGRND, -1) ||
       strsame (Command, CONTROL_PROXY_PURGE_REACTIVE, -1) ||
       strsame (Command, CONTROL_PROXY_PURGE_ROUTINE, -1) ||
       strsame (Command, CONTROL_PROXY_PURGE_HOST, -1) ||
       strsame (Command, CONTROL_PROXY_STATISTICS, -1) ||
       strsame (Command, CONTROL_PROXY_STOP_SCAN, -1) ||
       strsame (Command, CONTROL_RESTART, -1) ||
       strsame (Command, CONTROL_RESTART_NOW, -1) ||
       strsame (Command, CONTROL_RESTART_QUIET, -1) ||
       strsame (Command, CONTROL_SSL_CA_LOAD, -1) ||
       strsame (Command, CONTROL_THROTTLE_RELEASE, -1) ||
       strsame (Command, CONTROL_THROTTLE_TERMINATE, -1) ||
       strsame (Command, CONTROL_THROTTLE_ZERO, -1) ||
       strsame (Command, CONTROL_ZERO, -1))
   {
      if (strsame (Command, CONTROL_LIST, -1))
      {
         /**********************************/
         /* CLI special case, /DO=LIST/ALL */
         /**********************************/

         ServerCount = InstanceLockList (LockIndex, ", ", &cptr);
         WriteFao (String, sizeof(String), 0, "!UL server!%s; !AZ",
                   ServerCount, ServerCount ? cptr : "?");
         if (cptr) VmFree (cptr, FI_LI);
         return (String);
      }
      
      ServerCount = InstanceLockList (LockIndex, ", ", &cptr);
      InstanceLockNotifyNow (LockIndex, Command);
      WriteFao (String, sizeof(String), 0, "!UL instance!%s notified; !AZ",
                ServerCount, ServerCount ? cptr : "?");
      if (cptr) VmFree (cptr, FI_LI);
      return (String);
   }

   if (strsame (Command, CONTROL_LOG_FORMAT_AS,
                strlen(CONTROL_LOG_FORMAT_AS)) ||
       strsame (Command, CONTROL_LOG_OPEN_AS,
                strlen(CONTROL_LOG_OPEN_AS)) ||
       strsame (Command, CONTROL_LOG_PERIOD_AS,
                strlen(CONTROL_LOG_PERIOD_AS)) ||
       strsame (Command, CONTROL_LOG_REOPEN_AS,
                strlen(CONTROL_LOG_REOPEN_AS)))
      return ("!this directive is OBSOLETE");

   if (ControlDoAllHttpd)
   {
      if (strsame (Command, CONTROL_SSL_PKPASSWD, -1))
         return ("!cannot do this via /ALL");
   }

   return ("!directive not understood");
}

/*****************************************************************************/
/*
The EX (exclusive) lock enqueued by the controlling process has been dequeued
allowing this AST to be delivered.  The control directive is now in the lock
value block.  Do what it is requesting.  This AST is required to allow
AdminControl() to use it as part of an executing server.  The CONTROL_BUFFER
value indicates the server should examine 'ControlBuffer' in the global section
shared memory for a this-system-only command.
*/

ControlHttpdAst (struct lksb *lksbptr)

{
   int  clen, status,
        StartupMax;
   char  *cptr;

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                 "ControlHttpdAst() !&F !UL !&Z",
                 &ControlHttpdAst, lksbptr, (char*)lksbptr->lksb$b_valblk);

   /* get the lock value block written by the dequeued EX mode lock */
   cptr = ControlCommandPtr = (char*)lksbptr->lksb$b_valblk;
   clen = strlen(cptr);
   if (Debug) fprintf (stdout, "%d |%s|\n", clen, cptr);

   ControlMessage (cptr);

   if (WATCH_CAT && Watch.Category)
      WatchThis (NULL, FI_LI, WATCH_NOTICED, "HTTPD/DO=\'!AZ\'", cptr);

   if (strsame (cptr, CONTROL_AUTH_LOAD1, clen) ||
       strsame (cptr, CONTROL_AUTH_LOAD2, clen))
      AuthConfigInit ();
   else
   if (strsame (cptr, CONTROL_AUTH_PURGE, clen))
      AuthCachePurge (true);
   else
   if (strsame (cptr, CONTROL_AUTH_SKELETON, sizeof(CONTROL_AUTH_SKELETON)-1))
   {
      /* turn minutes set by ControlCommand() into seconds into the future */
      InstanceMutexLock (INSTANCE_MUTEX_HTTPD);
      if (HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond > 0 &&
          HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond <= 10080)
         HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond =
            HttpdTickSecond + HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond * 60;
      InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
   }
   else
   if (strsame (cptr, CONTROL_CACHE_ON, clen))
      CacheEnabled = true;
   else
   if (strsame (cptr, CONTROL_CACHE_OFF, clen))
      CacheEnabled = false;
   else
   if (strsame (cptr, CONTROL_CACHE_PURGE, clen))
      CachePurge (false, NULL, NULL);
   else
   if (strsame (cptr, CONTROL_DCL_DELETE, clen))
      DclControlPurgeScriptProcesses (true);
   else
   if (strsame (cptr, CONTROL_DCL_PURGE, clen))
      DclControlPurgeScriptProcesses (false);
   else
   if (strsame (cptr, CONTROL_DECNET_DISCONNECT, clen))
      DECnetControlDisconnect (false);
   else
   if (strsame (cptr, CONTROL_DECNET_PURGE, clen))
      DECnetControlDisconnect (true);
   else
   if (strsame (cptr, CONTROL_EXIT, clen))
   {
      /* stop the server from receiving incoming requests */
      NetShutdownServerSocket ();
      if (InstanceConnectCurrent)
      {
         /* this will now be handled by RequestEnd() */
         ControlExitRequested = true;
      }
      else
         ControlDelay (CONTROL_DELAY_EXIT);
   }
   else
   if (strsame (cptr, CONTROL_EXIT_NOW, clen))
      ControlDelay (CONTROL_DELAY_EXIT);
   else
   if (strsame (cptr, CONTROL_INSTANCE, sizeof(CONTROL_INSTANCE)-1))
   {
      while (*cptr && *cptr != '=') cptr++;
      if (*cptr == '=') cptr++;
      if (strsame (cptr, "max", -1))
         StartupMax = 0;
      else
      if (strsame (cptr, "cpu", -1))
         StartupMax = INSTANCE_PER_CPU;
      else
         StartupMax = atoi(cptr);
      InstanceMutexLock (INSTANCE_MUTEX_HTTPD);
      HttpdGblSecPtr->InstanceStartupMax = StartupMax;
      InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
   }
   else
   if (strsame (cptr, CONTROL_LOG_OPEN, clen))
      Logging (NULL, LOGGING_OPEN);
   else
   if (strsame (cptr, CONTROL_LOG_REOPEN, clen))
   {
      /* close then open */
      if (VMSok (Logging (NULL, LOGGING_CLOSE)))
         Logging (NULL, LOGGING_OPEN);
   }
   else
   if (strsame (cptr, CONTROL_LOG_CLOSE, clen))
      Logging (NULL, LOGGING_CLOSE);
   else
   if (strsame (cptr, CONTROL_LOG_FLUSH, clen))
      Logging (NULL, LOGGING_FLUSH);
   else
   if (strsame (cptr, CONTROL_MAP, clen))
      MapUrl_Load ();
   else
   if (strsame (cptr, CONTROL_PROXY_ON, clen))
   {
      InstanceMutexLock (INSTANCE_MUTEX_HTTPD);
      ProxyServingEnabled = ProxyAccountingPtr->ServingEnabled = true;
      InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
   }
   else
   if (strsame (cptr, CONTROL_PROXY_OFF, clen))
   {
      InstanceMutexLock (INSTANCE_MUTEX_HTTPD);
      ProxyServingEnabled = ProxyAccountingPtr->ServingEnabled = false;
      InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
   }
   else
   if (strsame (cptr, CONTROL_PROXY_PURGE_BCKGRND, clen))
      ProxyMaintScanBegin (PROXY_MAINT_SCAN_BCKGRND);
   else
   if (strsame (cptr, CONTROL_PROXY_PURGE_REACTIVE, clen))
      ProxyMaintScanBegin (PROXY_MAINT_SCAN_REACTIVE);
   else
   if (strsame (cptr, CONTROL_PROXY_PURGE_ROUTINE, clen))
      ProxyMaintScanBegin (PROXY_MAINT_SCAN_ROUTINE);
   else
/** TODO
   if (strsame (cptr, CONTROL_PROXY_PURGE_HOST, clen))
      ProxyResolveHostCache (NULL, NULL, 0);
   else
**/
   if (strsame (cptr, CONTROL_PROXY_STATISTICS, clen))
      ProxyMaintScanBegin (PROXY_MAINT_SCAN_STATISTICS);
   else
   if (strsame (cptr, CONTROL_PROXY_STOP_SCAN, clen))
      ProxyMaintScanBegin (PROXY_MAINT_SCAN_STOP);
   else
   if (strsame (cptr, CONTROL_RESTART, clen))
      ControlDelay (CONTROL_DELAY_RESTART);
   else
   if (strsame (cptr, CONTROL_RESTART_NOW, clen))
      ControlDelay (CONTROL_DELAY_RESTART_NOW);
   else
   if (strsame (cptr, CONTROL_RESTART_QUIET, clen))
      ControlDelay (CONTROL_DELAY_RESTART_QUIET);
   else
   if (strsame (cptr, CONTROL_SSL_CA_LOAD, clen))
      SesolaControlReloadCA ();
   else
   if (strsame (cptr, CONTROL_THROTTLE_RELEASE, clen))
      ThrottleControl (false, 0);
   else
   if (strsame (cptr, CONTROL_THROTTLE_TERMINATE, clen))
      ThrottleControl (true, 0);
   else
   if (strsame (cptr, CONTROL_THROTTLE_ZERO, clen))
      ThrottleZero ();
   else
   if (strsame (cptr, CONTROL_ZERO, clen))
      ControlZeroAccounting ();
   else
      ErrorNoticed (0, "unknown control directive", FI_LI);
}

/*****************************************************************************/
/*
Used to "sleep" with slightly greater granularity than sleep() provides.  This
is necessary due to the polling of events required under some circumstances.
Cannot be used if user-mode ASTs are unavailable (e.g. when processing commands
delivered using blocking lock ASTs).
*/

ControlSleep (int MilliSeconds)

{
   static unsigned long  OneMilliSecondDelta [2] = { -10000, -1 };

   int  status;
   unsigned long  ScratchBinTime [2];

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                 "ControlSleep() !UL", MilliSeconds);

   if (MilliSeconds)
   {
      memcpy (&ScratchBinTime, &OneMilliSecondDelta, sizeof(ScratchBinTime));
      status = lib$mult_delta_time (&MilliSeconds, &ScratchBinTime);
      if (VMSnok (status)) exit (status);

      status = sys$setimr (0, &ScratchBinTime, &ControlSleep, 0, 0);
      if (VMSnok (status)) exit (status);
      sys$hiber ();
      return;
   }

   sys$wake (0, 0);
}

/*****************************************************************************/
/*
Using the supplied PID fill out 'ControlPid', 'ControlProcessName, abnd
'ControlUserName' with the appropriate information.  If 'Pid' is zero then it
will be the datils of the current process.
*/ 

ControlAccount (unsigned long Pid)

{
   static BOOL  NoWorldPriv;
   static unsigned long  GetJpiControlFlags = JPI$M_IGNORE_TARGET_STATUS;

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char   *buf_addr;
      unsigned short  *short_ret_len;
   }
   JpiItems [] =
   {
      { sizeof(GetJpiControlFlags), JPI$_GETJPI_CONTROL_FLAGS,
        &GetJpiControlFlags, 0 },
      { sizeof(ControlPid), JPI$_PID, &ControlPid, 0 },
      { sizeof(ControlNodeName), JPI$_NODENAME, &ControlNodeName, 0 },
      { sizeof(ControlProcessName), JPI$_PRCNAM, &ControlProcessName, 0 },
      { sizeof(ControlUserName), JPI$_USERNAME, &ControlUserName, 0 },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr;
   IO_SB  IOsb;
 
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER, "ControlAccount() !8XL", Pid);

   /* use WORLD to allow access to other processes */
   status = sys$setprv (1, &WorldMask, 0, 0);
   if (VMSnok (status) || status == SS$_NOTALLPRIV)
   {
      if (!NoWorldPriv)
      {
         NoWorldPriv = true;
         fprintf (stdout,
"%%%s-W-CONTROL, installed without WORLD privilege\n",
            Utility);
      }
   }

   if (!NoWorldPriv && Pid)
   {
      status = sys$getjpiw (EfnWait, &Pid, 0, &JpiItems, &IOsb, 0, 0);
      if (VMSok (status)) status = IOsb.Status;
      if (Debug) fprintf (stdout, "sys$getjpiw() %%X%08.08X\n", status);
   }
   else
      status = SS$_NOPRIV;
   if (VMSok (status))
   {
      ControlNodeName[15] = '\0';
      for (cptr = ControlNodeName; *cptr && *cptr != ' '; cptr++);
      *cptr = '\0';
      (cptr = ControlProcessName)[15] = '\0';
      cptr--;
      while (cptr > ControlProcessName && *cptr == ' ') cptr--;
      *cptr = '\0';
      ControlUserName[12] = '\0';
      for (cptr = ControlUserName; *cptr && *cptr != ' '; cptr++);
      *cptr = '\0';
   }
   else
   {
      strcpy (ControlNodeName, "?");
      strcpy (ControlProcessName, "?");
      strcpy (ControlUserName, "?");
   }

   sys$setprv (0, &WorldMask, 0, 0);
}

/*****************************************************************************/
/*
Report a control request/response, to the process log (SYS$OUTPUT) and
optionally as OPCOM messages.  
*/

ControlMessage (char *Message)

{
   static char  LockValueBlock [16];
   static VMS_ITEM_LIST3  LkiItems [] =
   {
      { sizeof(LockValueBlock), LKI$_VALBLK, LockValueBlock, 0 },
      {0,0,0,0}
   };

   int  status;
   IO_SB  IOsb;

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                 "ControlMessage() !&Z", Message);

   memset (LockValueBlock, 0, sizeof(LockValueBlock));
   sys$setprv (1, &SysLckMask, 0, 0);
   status = sys$getlkiw (EfnWait,
      &InstanceLockTable[INSTANCE_CLUSTER_CONTROL].Lksb.lksb$l_lkid,
      &LkiItems, &IOsb, 0, 0, 0);
   sys$setprv (0, &SysLckMask, 0, 0);
   if (VMSok (status)) status = IOsb.Status;
   if (VMSnok (status)) ErrorNoticed (status, "sys$getlkiw()", FI_LI);

   if (sscanf (LockValueBlock, "%x", &ControlPid) < 1) ControlPid = 0;
   ControlAccount (ControlPid);

   WriteFaoStdout (
"%!AZ-!&?E\rI\r-CONTROL, !20%D, !8XL !AZ !AZ \"!AZ\", \'!AZ\'\n",
      Utility, Message[0] == '!', 0,
      ControlPid, ControlNodeName, ControlUserName, ControlProcessName,
      Message);

   if (OpcomMessages & OPCOM_CONTROL)
      WriteFaoOpcom (
"%!AZ-!&?E\rI\r-CONTROL, !8XL !AZ !AZ \"!AZ\", \'!AZ\'",
         Utility, Message[0] == '!',
         ControlPid, ControlNodeName, ControlUserName, ControlProcessName,
         Message);

   if (WATCH_CAT && Watch.Category)
   {
      WatchThis (NULL, FI_LI, WATCH_NOTICED, "HTTPD/DO=");
      WatchDataFormatted (
"%!AZ-!&?E\rI\r-CONTROL, !20%D, !8XL !AZ !AZ \"!AZ\", \'!AZ\'\n",
         Utility, Message[0] == '!', 0,
         ControlPid, ControlNodeName, ControlUserName, ControlProcessName,
         Message);
   }
}

/*****************************************************************************/
/*
Exits or restarts the server after a short delay.  This delay is introduced to
give a controlling request (e.g. via the Admin Menu) a chance to complete
processing (e.g. deliver success message) before the action is taken.  There is
an additional random delay introduced with multiple instances to prevent mass
suicide when RESTART=NOW and reducing the number of instances.

Is called directly by the requesting function with a parameter of
CONTROL_DELAY_EXIT or CONTROL_DELAY_RESTART.  A timer with AST delivery back to
this function is set, the AST parameter the same as the original with a flag
inserted indicating it really should be done *this* time!
*/

ControlDelay (int Action)

{
   int  status;
   unsigned long  DelayDelta [2];

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER, "ControlDelay() !UL", Action);

   if (!(Action & CONTROL_DELAY_DO))
   {
      DelayDelta [0] =  -2500000;  /* 250mS */
      /* if multi instances introduce a further, random delay of 250-750mS */
      if (InstanceNodeCurrent > 1 && !InstanceNodeSupervisor)
         DelayDelta[0] += -2500000 + (-50000 * HttpdNumTime[6]);
      DelayDelta [1] =  -1;
      status = sys$setimr (0, &DelayDelta, &ControlDelay,
                           Action | CONTROL_DELAY_DO, 0);
      if (VMSnok (status)) ErrorExitVmsStatus (status, "sys$setimr()", FI_LI);
      return;
   }

   switch (Action & ~CONTROL_DELAY_DO)
   {
      case CONTROL_DELAY_EXIT :

         ExitStatus = SS$_NORMAL;
         HttpdExit (&ExitStatus);
         /* record server event */
         GraphActivityEvent (ACTIVITY_DELPRC);
         sys$delprc (0, 0);

      case CONTROL_DELAY_RESTART :

         ControlRestartRequested = true;
         /* start the server supervisor in case it's not running */
         if (!HttpdTicking) HttpdTick (0);
         /* this will now be handled by InstanceSupervisor() */
         return;

      case CONTROL_DELAY_RESTART_QUIET :

         ControlRestartQuiet = true;
         /* start the server supervisor in case it's not running */
         if (!HttpdTicking) HttpdTick (0);
         /* this will now be handled by InstanceSupervisor() */
         return;

      case CONTROL_DELAY_RESTART_NOW :

         exit (SS$_NORMAL);

      default :

         ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
   }
}

/*****************************************************************************/
/*
Zero server accounting structure and service counters.
These are both process-local and node-global.
Each gets reset under differing circumstances.
*/ 

ControlZeroAccounting ()

{
   int  idx,
        StartupCount,
        ZeroedCount;

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

   if (WATCH_MODULE(WATCH_MOD__OTHER))
      WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                 "ControlZeroAccounting() !UL !&B",
                 InstanceNodeConfig, InstanceNodeSupervisor);

   /* these two get done on a per-process basis regardless */
   ThrottleZero ();
   NetServiceZeroAccounting ();

   /* in a multi-instance config, shared data only by the supervisor */
   if (InstanceNodeConfig > 1 && !InstanceNodeSupervisor) return;

   InstanceMutexLock (INSTANCE_MUTEX_HTTPD);

   StartupCount = AccountingPtr->StartupCount;
   ZeroedCount = AccountingPtr->ZeroedCount;
   memset (AccountingPtr, 0, sizeof(ACCOUNTING_STRUCT));
   AccountingPtr->StartupCount = StartupCount;
   AccountingPtr->ZeroedCount = ZeroedCount + 1;

   /* minimum duration must be set very high */
   AccountingPtr->ResponseDurationMin = 0xffffffff;

   memset (ProxyAccountingPtr, 0, sizeof(PROXY_ACCOUNTING_STRUCT));
   ProxyAccountingPtr->ServingEnabled = ProxyServingEnabled;
   ProxyAccountingPtr->FreeSpaceAvailable = ProxyCacheFreeSpaceAvailable;

   for (idx = 1; idx <= INSTANCE_MUTEX_COUNT; idx++)
      HttpdGblSecPtr->MutexCount[idx] = HttpdGblSecPtr->MutexWaitCount[idx] = 0;

   InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
}

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

