/*****************************************************************************/
/*
                               AuthAgent.c


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

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

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

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


This module interfaces with an authentication agent script.  These are CGIplus
scripts that perform a username/password validation role (authentication) or a
group membership role (authorization by virtue of group membership).  In this
environment the script has basically all the resources available to a CGIplus
script and must communicate with the server (to pass back the authorization
results) using "escaped" CGIplus data (see the DCL module for further detail).

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

The name of the script to be activated is derived from the realm or group name
(passed as 'AgentName') and the script directory contained in 'AUTH_AGENT_PATH'
(currently "/cgiauth-bin/", and could be remapped using HTTPD$MAP of course).

The WATCH facility is a valuable adjunct in understanding/debugging agent
script behaviour.


AUTHENTICATING A USERNAME/PASSWORD
----------------------------------
The transaction details are found in the following CGI variables.

WWW_AUTH_AGENT .................. "REALM" or other parameter
WWW_AUTH_PASSWORD ............... user supplied case-sensitive password
WWW_AUTH_REALM .................. realm name (same as agent name)
WWW_AUTH_REALM_DESCRIPTION ...... realm description user is prompted with
WWW_REMOTE_USER ................. case-sensitive username

Valid responses (digits and 'access' are mandatory, other text is optional):

'000 any text' ........... ignored by the server, provides WATCHable trace info 
'100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite)
'100 NOCACHE' ............ do not cache the results of this authorization
'100 REMOTE-USER name .... provide user name (authenticated some non-401 way)
'100 VMS-USER name ....... this username is a VMS username (see note below)
'100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header)
'100 USER any text' ...... provide user details (only after 200 response)
'100 REASON any text' .... reason authentication was denied
'200 access' ............. username/password verified
                           access: "READ", "WRITE", "READ+WRITE", "FULL"
'302 location ............ redirect to specified location
                           e.g. http://the.host.name/the/path
                                //the.host.name/the/path
                                ///the/path
'401 reason' ............. username/password did not verify
'401 "any-text"' ......... (quoted) used as the browser authorization prompt
'403 reason' ............. access is forbidden
'500 description' ........ script error to be reported via server

VMS-USER issues: when a VMS-USER is passed back to the server the username
undergoes all VMS authorization processing (e.g. ID, prime days, etc) - except
password checking, it is assumed the agent has authenticated the username.  The
access level (R, R+W, etc.) is derived from the SYSUAF information - unless the
agent *subsequently* provides a "200 access" callout.  The user details come
from the SYSUAF - unless the agent *subsequently* provides a "100 USER details"
callout.


ESTABLISHING GROUP MEMBERSHIP
-----------------------------
The transaction details are found in the following CGI variables.

WWW_AUTH_AGENT .................. "GROUP"
WWW_AUTH_GROUP .................. name of group
WWW_REMOTE_USER ................. case-sensitive username

Valid responses (digits are mandatory, other text is optional):

'000 any text' ........... ignored by the server, provides WATCHable trace info 
'100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite)
'100 NOCACHE' ............ do not cache the results of this authorization
'100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header)
'200 any text' ........... indicates group membership
'302 location ............ redirect to specified location
                           e.g. http://the.host.name/the/path
                                //the.host.name/the/path
                                ///the/path
'403 reason' ............. indicates not a group member
'500 description' ........ script error to be reported via server


VERSION HISTORY
---------------
08-MAR-2003  MGD  add '100 REASON any text'
04-AUG-2001  MGD  support module WATCHing
02-AUG-2001  MGD  bugfix; allow agent to accept 'CGIPLUS:' directive
07-DEC-2000  MGD  agent can now '100 SET-COOKIE rfc2109-cookie'
27-NOV-2000  MGD  bugfix; ensure a mapping rule exists for the agent
09-JUN-2000  MGD  allow "302 location" redirection response
28-AUG-1999  MGD  initial, for v6.1
*/
/*****************************************************************************/

#ifdef WASD_VMS_V6
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 60000000
#undef __CRTL_VER
#define __CRTL_VER 60000000
#endif

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

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

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

#define WASD_MODULE "AUTHAGENT"

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

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

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

extern char  ErrorSanityCheck[];
extern ACCOUNTING_STRUCT  *AccountingPtr;
extern MSG_STRUCT  Msgs;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initiate an authenication agent script.  After calling this function all
authentication processing occurs asynchronously.
*/ 

AuthAgentBegin
(
REQUEST_STRUCT *rqptr,
char *AgentName,
REQUEST_AST AgentCalloutFunction
)
{
   char *cptr;
   char  AgentFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         AgentScriptName [SCRIPT_NAME_SIZE+1],
         Scratch [256];

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AuthAgentBegin() !&Z !&X",
                 AgentName, AgentCalloutFunction);

   /* after calling this function authorization completes asynchronously! */
   rqptr->rqAuth.AstFunction = rqptr->rqAuth.AstFunctionBuffer;
   rqptr->rqAuth.FinalStatus = AUTH_PENDING;

   strcpy (Scratch, AUTH_AGENT_PATH);
   strcat (Scratch, AgentName);

   AgentFileName[0] = AgentScriptName[0] = '\0';
   cptr = MapUrl_Map (Scratch, 0,
                      NULL, 0,
                      AgentScriptName, sizeof(AgentScriptName),
                      AgentFileName, sizeof(AgentFileName),
                      NULL, 0,
                      NULL, rqptr);
   if (AgentScriptName[0] == '+') AgentScriptName[0] = '/';
   if ((!cptr[0] && cptr[1]) || !AgentScriptName[0] || !AgentFileName[0])
   {
      /* either mapping error or no rule to map the agent */
      ErrorGeneral (rqptr, MsgFor(rqptr,MSG_AUTH_AGENT_MAPPING), FI_LI);
      SysDclAst (AgentCalloutFunction, rqptr);
      return;
   }

   /* indicate it's an agent script, not a CGI-compliant script */
   rqptr->rqCgi.AgentScript = true;

   DclBegin (rqptr, AgentCalloutFunction, NULL,
             AgentScriptName, NULL, AgentFileName, NULL, &AuthAgentCallout);
}

/*****************************************************************************/
/*
This is the function called each time the agent script outputs escaped data to
the server.  It must check for beginning and end of agent processing (indicated
by various states of the the 'OutputPtr' and 'OutputCount' storage), and
appropriately process the status responses output by the agent.
*/ 

AuthAgentCallout (REQUEST_STRUCT *rqptr)

{
   int  idx,
        status,
        CgiPlusLifeTime,
        Length,
        OutputCount;
   char  *cptr, *sptr, *zptr,
         *OutputPtr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AuthAgentCallout() !&Z",
                 rqptr->rqAuth.PathParameterPtr);

   OutputPtr = rqptr->rqCgi.CalloutOutputPtr;
   OutputCount = rqptr->rqCgi.CalloutOutputCount;

   if (!OutputPtr && OutputCount == -1)
   {
      /***************/
      /* agent begin */
      /***************/

      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_AUTH))
         WatchThis (rqptr, FI_LI, WATCH_AUTH,
                    "CALLOUT \"!AZ\" begin", rqptr->rqAuth.PathParameterPtr);
      return;
   }

   if (!OutputPtr && OutputCount == 0)
   {
      /*************/
      /* agent end */
      /*************/

      if (Debug) fprintf (stdout, "agent EOT!\n");

      if (WATCHING(rqptr) &&
          WATCH_CATEGORY(WATCH_AUTH))
         WatchThis (rqptr, FI_LI, WATCH_AUTH,
                    "CALLOUT \"!AZ\" end", rqptr->rqAuth.PathParameterPtr);

      /* always ensure any authentication agent information is cancelled! */
      rqptr->rqAuth.PathParameterPtr = "";
      rqptr->rqAuth.PathParameterLength = 0;

      return;
   }

   /**************/
   /* agent data */
   /**************/

   if (Debug) fprintf (stdout, "%d |%s|\n", OutputCount, OutputPtr);

   if (WATCHING(rqptr) &&
       WATCH_CATEGORY(WATCH_AUTH))
   {
      WatchThis (rqptr, FI_LI, WATCH_AUTH,
                 "CALLOUT \"!AZ\" !UL bytes",
                 rqptr->rqAuth.PathParameterPtr, OutputCount);
      WatchDataDump (OutputPtr, OutputCount);
      /* if it's trace information then provide it slightly more readably */
      if (!strncmp (OutputPtr, "000 ", 4)) WatchData (OutputPtr, OutputCount);
   }

   if (strsame (OutputPtr, "CGIPLUS:", Length=8) ||
       strsame (OutputPtr, "!CGIPLUS:", Length=9))
   {
      /************/
      /* cgiplus: */
      /************/

      for (cptr = OutputPtr+Length; *cptr && isspace(*cptr); cptr++);
      if (strsame (cptr, "STRUCT", 6))
         rqptr->DclTaskPtr->CgiPlusVarStruct = true;
      else
      if (strsame (cptr, "RECORD", 6))
         rqptr->DclTaskPtr->CgiPlusVarStruct = false;
      else
         AuthAgentCalloutResponseError (rqptr);
      return;
   }

   /******************/
   /* agent response */
   /******************/

   if (OutputCount <= 4 ||
       !isdigit(OutputPtr[0]) ||
       !isdigit(OutputPtr[1]) ||
       !isdigit(OutputPtr[2]) ||
       OutputPtr[3] != ' ')
   {
      /* agent response error */
      AuthAgentCalloutResponseError (rqptr);
      return;
   }

   if (!strcmp (rqptr->rqAuth.PathParameterPtr, "GROUP"))
   {
      if (!strncmp (OutputPtr, "200 ", 4))
      {
         /* a group member */
         if (rqptr->rqAuth.FinalStatus != STS$K_ERROR)
            rqptr->rqAuth.FinalStatus = SS$_NORMAL;
         return;
      }
      if (!strncmp (OutputPtr, "403 ", 4))
      {
         /* not a group member */
         rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_GROUP;
         return;
      }
   }

   if (!strncmp (OutputPtr, "000 ", 4))
   {
      /* just WATCHable debug information */
      return;
   }

   if (!strncmp (OutputPtr, "100 ", 4))
   {
      /* informational */
      if (strsame (OutputPtr+4, "LIFETIME ", 9))
      {
         /* set lifetime of agent script */
         for (cptr = OutputPtr+13; *cptr && !isdigit(*cptr); cptr++);
         if (isdigit(*cptr))
         {
            /* zero makes the script immune to supervisor purging */
            if ((CgiPlusLifeTime = atoi(cptr)) == 0)
               CgiPlusLifeTime = DCL_DO_NOT_DISTURB;
            rqptr->DclTaskPtr->LifeTimeSecond = CgiPlusLifeTime;
            return;
         }               
      }
      if (strsame (OutputPtr+4, "NOCACHE", 7))
      {
         /* do not cache this authorization */
         rqptr->rqAuth.NoCache = true;
         return;
      }
      if (strsame (OutputPtr+4, "REASON ", 7))
      {
         /* reason for authentication failure (included in message) */
         for (cptr = OutputPtr+11; *cptr && ISLWS(*cptr); cptr++);
         for (sptr = cptr; *sptr && NOTEOL(*sptr); sptr++);
         *sptr = '\0';
         rqptr->rqAuth.ReasonLength = Length = sptr - cptr + 1;
         rqptr->rqAuth.ReasonPtr = VmGetHeap (rqptr, Length);
         strcpy (rqptr->rqAuth.ReasonPtr, cptr);
         return;
      }
      if (strsame (OutputPtr+4, "REMOTE-USER ", 12))
      {
         /* user details */
         if (OutputPtr[OutputCount-1] == '\n') OutputPtr[--OutputCount] = '\0'; 
         zptr = (sptr = rqptr->RemoteUser) + sizeof(rqptr->RemoteUser)-1;
         for (cptr = OutputPtr+16; *cptr && ISLWS(*cptr); cptr++);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
            *sptr++ = *cptr++;
         if (sptr >= zptr)
         {
            rqptr->RemoteUser[0] = '\0';
            rqptr->rqAuth.FinalStatus = STS$K_ERROR;
            ErrorGeneralOverflow (rqptr, FI_LI);
            return;
         }
         *sptr = '\0';
         rqptr->RemoteUserLength = sptr - rqptr->RemoteUser;
         return;
      }
      if (strsame (OutputPtr+4, "VMS-USER ", 9))
      {
         /* user details */
         zptr = (sptr = rqptr->RemoteUser) + sizeof(rqptr->RemoteUser)-1;
         for (cptr = OutputPtr+13; *cptr && ISLWS(*cptr); cptr++);
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
            *sptr++ = *cptr++;
         if (sptr >= zptr)
         {
            rqptr->RemoteUser[0] = '\0';
            rqptr->rqAuth.FinalStatus = STS$K_ERROR;
            ErrorGeneralOverflow (rqptr, FI_LI);
            return;
         }
         *sptr = '\0';
         rqptr->RemoteUserLength = sptr - rqptr->RemoteUser;

         if (VMSok (status = AuthVmsGetUai (rqptr, rqptr->RemoteUser)))
         {
            if (VMSok (status = AuthVmsVerifyUser (rqptr)))
            {
               /* authenticated ... user can do anything (the path allows!) */
               rqptr->rqAuth.UserCan = AUTH_READWRITE_ACCESS;
               rqptr->rqAuth.SysUafAuthenticated = true;
            }
         }
         rqptr->rqAuth.FinalStatus = status;

         return;
      }
      if (strsame (OutputPtr+4, "SET-COOKIE ", 11))
      {
         /* add a cookie to the header */
         if (OutputPtr[OutputCount-1] == '\n') OutputPtr[--OutputCount] = '\0'; 
         for (cptr = OutputPtr+15; *cptr && ISLWS(*cptr); cptr++);
         for (sptr = cptr; *sptr && NOTEOL(*sptr); sptr++);
         *sptr = '\0';
         Length = sptr - cptr + 1;
         for (idx = 0; idx < RESPONSE_COOKIE_MAX; idx++)
         {
            if (!rqptr->rqResponse.CookiePtr[idx])
            {
               rqptr->rqResponse.CookiePtr[idx] = VmGetHeap (rqptr, Length);
               strcpy (rqptr->rqResponse.CookiePtr[idx], cptr);
               return;
            }
         }
      }
      if (strsame (OutputPtr+4, "USER ", 5))
      {
         /* user details */
         for (cptr = OutputPtr+9; *cptr && ISLWS(*cptr); cptr++);
         for (sptr = cptr; *sptr && NOTEOL(*sptr); sptr++);
         *sptr = '\0';
         rqptr->rqAuth.UserDetailsLength = Length = sptr - cptr + 1;
         rqptr->rqAuth.UserDetailsPtr = VmGetHeap (rqptr, Length);
         strcpy (rqptr->rqAuth.UserDetailsPtr, cptr);
         return;
      }
      AuthAgentCalloutResponseError (rqptr);
      return;
   }

   if (!strncmp (OutputPtr, "200 ", 4))
   {
      /* authenticated */
      rqptr->rqAuth.FinalStatus = SS$_NORMAL;
      if (strsame (OutputPtr+4, "FULL", 4))
      {
         rqptr->rqAuth.UserCan = AUTH_READWRITE_ACCESS;
         if (rqptr->rqAuth.FinalStatus != STS$K_ERROR)
            rqptr->rqAuth.FinalStatus = SS$_NORMAL;
         return;
      }
      if (strsame (OutputPtr+4, "READ+WRITE", 10))
      {
         rqptr->rqAuth.UserCan = AUTH_READWRITE_ACCESS;
         if (rqptr->rqAuth.FinalStatus != STS$K_ERROR)
            rqptr->rqAuth.FinalStatus = SS$_NORMAL;
         return;
      }
      if (strsame (OutputPtr+4, "READ", 4))
      {
         rqptr->rqAuth.UserCan = AUTH_READONLY_ACCESS;
         if (rqptr->rqAuth.FinalStatus != STS$K_ERROR)
            rqptr->rqAuth.FinalStatus = SS$_NORMAL;
         return;
      }
      if (strsame (OutputPtr+4, "WRITE", 5))
      {
         rqptr->rqAuth.UserCan = AUTH_WRITEONLY_ACCESS;
         if (rqptr->rqAuth.FinalStatus != STS$K_ERROR)
            rqptr->rqAuth.FinalStatus = SS$_NORMAL;
         return;
      }
      AuthAgentCalloutResponseError (rqptr);
      return;
   }

   if (!strncmp (OutputPtr, "302 ", 4))
   {
      /* redirection */
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_REDIRECT;
      cptr = OutputPtr + 4;
      while (*cptr && ISLWS(*cptr)) cptr++;
      for (sptr = cptr; *sptr && NOTEOL(*sptr) && !ISLWS(*sptr); sptr++);
      *sptr = '\0';
      rqptr->rqResponse.LocationPtr = sptr = VmGetHeap (rqptr, sptr-cptr);
      while (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';
      return;
   }

   if (!strncmp (OutputPtr, "401 ", 4))
   {
      /* not authenticated */
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_LOGIN;
      cptr = OutputPtr + 4;
      while (*cptr && ISLWS(*cptr)) cptr++;
      if (*cptr == '\"')
      {
         /* realm description */
         for (sptr = cptr+1; *sptr && *sptr != '\"'; sptr++);
         if (*sptr)
            zptr = sptr + 1;
         else
            zptr = NULL;
         *sptr = '\0';
         rqptr->rqAuth.RealmDescrPtr = sptr = VmGetHeap (rqptr, sptr-cptr);
         cptr++;
         /* trim leading white-space */
         while (*cptr && ISLWS(*cptr)) cptr++;
         while (*cptr) *sptr++ = *cptr++;
         *sptr = '\0';
         /* trim trailing white-space */
         if (sptr > rqptr->rqAuth.RealmDescrPtr) sptr--;
         while (sptr > rqptr->rqAuth.RealmDescrPtr && ISLWS(*sptr)) sptr--;
         if (Debug) fprintf (stdout, "|%s|\n", rqptr->rqAuth.RealmDescrPtr); 
      }
      return;
   }

   if (!strncmp (OutputPtr, "403 ", 4))
   {
      /* not authorized */
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   if (!strncmp (OutputPtr, "500 ", 4))
   {
      /* report this error via the server */
      rqptr->rqAuth.FinalStatus = STS$K_ERROR;
      if (OutputPtr[OutputCount-1] == '\n') OutputPtr[--OutputCount] = '\0'; 
      ErrorGeneral (rqptr, OutputPtr+4, FI_LI);
      return;
   }

   AuthAgentCalloutResponseError (rqptr);
}

/*****************************************************************************/
/*
Simple way to generate this particular error message from various points.
*/ 

AuthAgentCalloutResponseError (REQUEST_STRUCT *rqptr)

{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH,
                 "AuthAgentCalloutResponseError()\n");

   ErrorGeneral (rqptr, MsgFor(rqptr,MSG_AUTH_AGENT_RESPONSE), FI_LI);

   rqptr->rqAuth.FinalStatus = STS$K_ERROR;
}

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