/*****************************************************************************/
/*
                                 AuthToken.c


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

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

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

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

This module provides functions related to the authentication of
case-insensitive usernames via simple lists of names (and optional
case-insensitive passwords and user detail).

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


TOKEN AUTHORIZATION
-------------------
This is a niche authorisation environment for addressing niche requirements.

Reflect the authorisation applied in one environment to another using a
short-lived token supplied as a cookie.  Originally devised to allow controlled
access to very large datasets without the overhead of SSL in the transmission
but with the access credentials supplied in the privacy of an SSL connection. 
The cookie contains NO CREDENTIAL data at all and the agent manages an internal
database (in memory) of these so it can determine whether any supplied token is
valid and when that token has expired.  By default (and commonly) token
authorisation occurs in non-SSL space (http:) and the credential authorisation
in SSL space (https:) although it is possible to have the two differentiated by
port only.  The path and realm specified in both must be the same.

A common scenario is where the client starts off attempting to access a
resource in non-SSL space which is controlled by TOKEN authentication.  In the
first instance the code detects there is no access token present and redirects
the client (browser) to the SSL equivalent of that space, where credentials can
be supplied encrypted.  In this scenario this SSL area is controlled by WASD
SYSUAF  authentication (or SSL client certificate for example, or other) and
the username/password is prompted for.  When correctly entered this generates a
token.  The token is stored (with corresponding detail) as a record in a global
memory buffer and then returned to the browser as a set-cookie value (during
the redirect described in the next paragraph).

With the token data stored the browser is transparently redirected back to the
non-SSL space where the actual access is to be undertaken, this time the
browser presenting the cookie containing the token to the TOKEN authentication
code originally initiating the described activity.  This code examines the
token, looking it up in the global memory buffer.  If found, has originated
from the same IP address, represents the same authentication realm,  has not
expired, then it allows the non-SSL space access to proceed, and in the
original case the dataset transfer is initiated (in unencrypted clear-text). 
If the token is not found in the database or has expired, then the process is
repeated with a redirect back into SSL space.  If the realms differ a 403
forbidden response is issued (see configuration below).

The token is a significant sequence of pseudo-random characters, is
short-lived (configurable as anything from a few seconds to a few tens of
seconds, in fact any amount of time the site is comfortable with), and as a
consequence is frequently regenerated.  The token is just that, containing no
actual credential data at all.  It might be possible to sniff but as it
contains nothing of value in itself, expires relatively quickly, and has an
originating IP address check, the fairly remote risk of playback is just that.

The authentication agent does all the work, implicitly redirecting the user
from non-SSL space to SSL space for the original authentication, and then
back again with the token used for access in the non-SSL space.  With the
expiry of a token it undertakes that cycle again, redirecting back to the
SSL-space where the browser-cached credentials will be supplied automatically
allowing the fresh token to be issued, and then redirected back into non-SSL
space for access.  To emphasise - all this is transparent to the user.

As a consequence of this model the resource being controlled can ONLY be
accessed from non-SSL space using the controlled path.  To access the same
resource from SSL space a distinct path to the resource must be provided.

Has been developed and tested against Chrome 21.0, Firefox 15.0, MSIE 9.0,
Opera 12.0 (with some tweaking) and Safari 6.0.


TOKEN RULE
----------
The token comprises two mandatory and two optional elements.  The mandatory are
the TOKEN authentication type and the preceding realm string.  This realm
string must be the same as that used to authenticate against (see below).  The
optional elements are the leading (and throwaway) realm description and the
realm parameter (see below).

Using defaults ("WASDtoken" for the token name), default https: port (443)

  [EXAMPLE_REALM=TOKEN]
  /a/path/* r+w

Specify a non-default SSL (default https:) port with default token name
("WASDtoken") using a leading ':'

  [EXAMPLE_REALM=TOKEN+:7443]
  /a/path/* r+w

Specify a non-default http: scheme (non-SSL) port with token name default
("WASDtoken") using the scheme ':' to also delimit the port

  [EXAMPLE_REALM=TOKEN+http:7443]
  /a/path/* r+w

An optional and throwaway realm description

  ["throwaway description"=EXAMPLE_REALM=TOKEN]
  /a/path/* r+w

An optional and throwaway realm description, specify non-default https: port
7443 (realm parameter can optionally be delimited by quotes)

  ["throwaway description"=EXAMPLE_REALM=TOKEN+":7443"]
  /a/path/* r+w


TOKEN PARAMETER
---------------
Token parameters can be used with both TOKEN and other credential realms,
although the lifetime parameter only applies to non-TOKEN realms.  A realm
parameter is appended to the realm type using a plus symbol and optional quoted
string.  Parameter elements are delimited by a comma.

For example a non-standard port specified with a leading ':'

  ["VMS credentials"=EXAMPLE=ID+"token=:7080"]
  /a/path/* r+w

A non-default token (cookie) name may be specified

  ["VMS credentials"=EXAMPLE=ID+"token=example-token-name"]
  /a/path/* r+w

Variations on the token (cookie) name may be specified using an integer. 
Instead of the default "WASDtoken" cookie name the following example would use
"WASDtoken10".

  ["VMS credentials"=EXAMPLE=ID+"&10"]
  /a/path/* r+w

A non-default token lifetime may be specified using a leading '#' symbol, in
this case one of 300 seconds (or 5 minutes)

  ["VMS credentials"=EXAMPLE=ID+"token=example-token-name,#300"]
  /a/path/* r+w

By default credentials are supplied in SSL space and the token is used in
non-SSL space (by redirect) but a redirect back into SSL space may be effected
using the "https:" keyword parameter (on a different port!)

  ["VMS credentials"=EXAMPLE=ID+"token=example-token-name,https:7080"]
  /a/path/* r+w


CONFIGURATION
-------------
The automatic authorisation and redirection occurs using a combination of two
distinguishable authorisation rules, one for supplying the credentials, the
other for using the token for authorisation.  In this example (and commonly)
the resources are at "/location/" and the configuration accepts user-supplied
credentials in SSL space and uses the token in non-SSL space.  The asterisk
just indicates that in the absence of any other parameter this authorisation
rule has a complementary token rule.

  # WASD_CONFIG_AUTH
  if (ssl:) 
     ["VMS credentials"=WASD_VMS_RW=id+"TOKEN=*"]
     /location/* r+w
  else
     [WASD_VMS_RW=TOKEN]
     /location/* r+w
  endif

And in this example, the same arrangement but with non-standard ports.

  # WASD_CONFIG_AUTH
  if (ssl:) 
     ["VMS credentials"=WASD_VMS_RW=id+"TOKEN=:7080"]
     /location/* r+w
  else
     [WASD_VMS_RW=TOKEN+"TOKEN=:7443"]
     /location/* r+w
  endif

To prevent potential "thrashing", where multiple, distinct realms within a
single request are authorised using tokens, corresponding multiple token
(cookie) names must be used.  It is expected that this would be an uncommon but
not impossible scenario.  "Thrashing" would be a result of authorisation
associated with a single, particular token name.  Where a realm differs from a
previous token generated another is required.  The TOKEN authorsation scheme
forces the use of distinct token names by 403-forbidding change of realm using
the one token.   Use explicitly specified, independent token (cookie) names, or
the "&<integer>" syntax to append the integer to the base token name, ensuring
the complementary rules are using the same name/integer.

  # WASD_CONFIG_AUTH
  if (ssl:) 
     ["VMS credentials"=WASD_VMS_RW=id+"TOKEN=&42"]
     /location/* r+w
  else
     [WASD_VMS_RW=TOKEN+"TOKEN=&42"]
     /location/* r+w
  endif

For the final example, the token is contained in the non-default cookie named
"WaSd_example" and the authentication performed using an X509 client
certificate (which can only be supplied via SSL).

  # WASD_CONFIG_AUTH
  if (ssl:) 
     [X509+"TOKEN=WaSd_example"]
     /location/* r+w
  else
     [X509=TOKEN+"TOKEN=WaSd_example"]
     /location/* r+w
  endif


VERSION HISTORY
---------------
18-SEP-2012  MGD  initial
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#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 <secdef.h>
#include <ssdef.h>
#include <stsdef.h>

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

#define WASD_MODULE "AUTHTOKEN"

#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

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

int  AuthTokenCurrentCount,
     AuthTokenGblSecPages,
     AuthTokenGblSecSize,
     AuthTokenRecordMax,
     AuthTokenRecordSize = sizeof(AUTH_TOKEN_RECORD);

AUTH_TOKEN_RECORD  *AuthTokenDataBase;

AUTH_TOKEN_GBLSEC  *AuthTokenGblSecPtr;

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

extern BOOL  AuthRealmToken,
             HttpdServerStartup;

extern int  GblPageCount,
            GblSectionCount,
            HttpdTickSecond,
            InstanceNodeConfig,
            InstanceEnvNumber,
            OpcomMessages,
            AuthTokenGblSecVersion;

extern int  ToLowerCase[],
            ToUpperCase[];

extern unsigned long  GblSecPrvMask[];

extern char  ErrorSanityCheck[];

extern CONFIG_STRUCT  Config;
extern HTTPD_PROCESS  HttpdProcess;
extern SYS_INFO  SysInfo;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initialize the token authentication/authorization environment.
*/ 

AuthTokenInit ()

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH, "AuthTokenInit()");

   if (!AuthRealmToken) return;

   if (HttpdServerStartup)
   {
      AuthTokenRecordMax = Config.cfAuth.TokenEntriesMax;
      if (AuthTokenRecordMax < 0)
      {
         AuthTokenRecordMax = 0;
         FaoToStdout ("%HTTPD-W-AUTHTOKEN, disabled\n");
         return;
      }
      if (!AuthTokenRecordMax)
         AuthTokenRecordMax = AUTH_TOKEN_DEFAULT_RECORD_MAX;
      else
      if (AuthTokenRecordMax < AUTH_TOKEN_RECORD_MIN)
         AuthTokenRecordMax = AUTH_TOKEN_RECORD_MIN;

      AuthTokenGblSecInit ();
   }

   if (!AuthTokenGblSecPtr) return;

   AuthTokenDataBase = (AUTH_TOKEN_RECORD*)(&AuthTokenGblSecPtr->RecordPool);
}

/****************************************************************************/
/*
This function runs after authentication and authorisation has occured (often
but not exclusively in SSL space).  A pseudo-random token is generated and set
as a cookie during the redirection back to the non-encrypted space.  This
token, along with a expiry time and client IP address, is added to the global
section as a record.  It then redirects to the non-SSL resource.  If anything
falls over it returns false other true to indicate a redirect is effect.
*/ 

void AuthTokenGenerate (REQUEST_STRUCT* rqptr)

{
   static unsigned long  PrevSeed;

#ifdef __VAX
   unsigned long  RandomNumber;
   unsigned long  GetTimFiller;
#else  /* Alpha or ia64 */
   unsigned __int64  RandomNumber;
#endif /* #ifdef VAX */

   int  cnt, idx,
        CookieSize,
        LocationSize,
        SemiColon,
        TokenIntegerLength = 0,
        TokenLifeTime,
        TokenMaxAgeLength,
        TokenNameLength,
        TokenPortLength = 0;
   char  *cptr, *sptr, *zptr,
         *CookiePtr,
         *SchemePtr = "http:",
         *TokenMaxAgePtr = NULL,
         *TokenIntegerPtr = NULL,
         *TokenNamePtr = NULL,
         *TokenPortPtr = NULL,
         *TokenValuePtr;
   char  TokenName [64],
         TokenValue [AUTH_SIZE_TOKEN_VALUE+1];
   AUTH_TOKEN_RECORD  *atkptr;
   DICT_ENTRY_STRUCT  *denptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_AUTH))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH, "AuthTokenGenerate()");

   if (!AuthTokenRecordMax)
   {
      if (WATCHING (rqptr, WATCH_AUTH))
          WatchThis (WATCHITM(rqptr), WATCH_AUTH, "TOKEN disabled 0 entries");
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   if (!rqptr->rqHeader.HostPtr ||
       !rqptr->rqHeader.RequestUriPtr)
   {
      if (WATCHING (rqptr, WATCH_AUTH))
          WatchThis (WATCHITM(rqptr), WATCH_AUTH, "!AZ", ErrorSanityCheck);
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   if (rqptr->rqAuth.RealmParamPtr &&
       *(cptr = rqptr->rqAuth.RealmParamPtr))
   {
      /* shouldn't even get here without this! */
      if (strsame (cptr, "TOKEN=", 6))
      {
         /**************/
         /* parameters */
         /**************/

         cptr += 6;
         while (*cptr)
         {
            if (*cptr == '*')
               cptr++;
            else
            if (*cptr == ':' && isdigit(*cptr+1))
            {
               TokenPortPtr = ++cptr;
               while (isdigit(*cptr)) cptr++;
               TokenPortLength = cptr - TokenPortPtr;
            }
            else
            if (*cptr == '#' && isdigit(*(cptr+1)))
            {
               TokenMaxAgePtr = ++cptr;
               while (isdigit(*cptr)) cptr++;
               TokenMaxAgeLength = cptr - TokenMaxAgePtr;
            }
            else
            if (*cptr == '&' && isdigit(*(cptr+1)))
            {
               TokenIntegerPtr = ++cptr;
               while (isdigit(*cptr)) cptr++;
               TokenIntegerLength = cptr - TokenIntegerPtr;
            }
            else
            if (strsame (cptr, "http:", 5))
            {
               SchemePtr = "http:";
               cptr += 5;
               if (isdigit(*cptr)) cptr--;
            }
            else
            if (strsame (cptr, "https:", 6))
            {
               SchemePtr = "https:";
               cptr += 6;
               if (isdigit(*cptr)) cptr--;
            }
            else
            if (isalpha(*cptr))
            {
               for (TokenNamePtr = cptr; *cptr && isalpha(*cptr); cptr++);
               TokenNameLength = cptr - TokenNamePtr;
            }
            else
               while (*cptr && !(ISLWS(*cptr) || *cptr == ',')) cptr++;

            while (*cptr && (ISLWS(*cptr) || *cptr == ',')) cptr++;
         }
      }
      else
      {
         if (WATCHING (rqptr, WATCH_AUTH))
             WatchThis (WATCHITM(rqptr), WATCH_AUTH, "!AZ", ErrorSanityCheck);
         rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
         return;
      }
   }

   if (!TokenNamePtr)
   {
      TokenNamePtr = AUTH_TOKEN_NAME_DEFAULT;
      TokenNameLength = sizeof(AUTH_TOKEN_NAME_DEFAULT)-1;
   }
   zptr = (sptr = TokenName) + sizeof(TokenName)-1;
   for (cptr = TokenNamePtr;
        TokenNameLength-- && sptr < zptr;
        *sptr++ = *cptr++);
   if (TokenIntegerLength)
      for (cptr = TokenIntegerPtr;
           TokenIntegerLength-- && sptr < zptr;
           *sptr++ = *cptr++);
   *sptr = '\0';
   TokenNameLength = sptr - TokenName;

   if (!TokenMaxAgePtr)
   {
      TokenMaxAgePtr = AUTH_TOKEN_MAXAGE_DEFAULT;
      TokenMaxAgeLength = sizeof(AUTH_TOKEN_MAXAGE_DEFAULT)-1;
   }

   TokenLifeTime = atoi(TokenMaxAgePtr);
   if (TokenLifeTime < AUTH_TOKEN_MAXAGE_MIN)
   {
      TokenMaxAgePtr = AUTH_TOKEN_MAXAGE_DEFAULT;
      TokenMaxAgeLength = sizeof(AUTH_TOKEN_MAXAGE_DEFAULT)-1;
      TokenLifeTime = atoi(TokenMaxAgePtr);
   }

   TokenValuePtr = AuthTokenFromCookie (rqptr, TokenName);

   /****************************/
   /* generate fresh PRN token */
   /****************************/

   sys$gettim (&RandomNumber);
   RandomNumber += PrevSeed;
   zptr = (sptr = TokenValue) + sizeof(TokenValue)-1;
   while (sptr < zptr)
   {
      /* cheap (no subroutine call) MTH$RANDOM() */
      RandomNumber = RandomNumber * 69069 + 1;
      cptr = (char*)&RandomNumber;
      for (cnt = sizeof(RandomNumber); cnt && sptr < zptr; cnt--)
      {
         if ((*cptr >= 'a' && *cptr <= 'z') ||
             (*cptr >= 'A' && *cptr <= 'Z') ||
             (*cptr >= '0' && *cptr <= '9')) *sptr++ = *cptr;
         cptr++;
      }
   }
   *sptr = '\0';
   PrevSeed = RandomNumber & 0xffff;

   /*******************/
   /* update database */
   /*******************/

   if (WATCHING (rqptr, WATCH_AUTH))
      WatchThis (WATCHITM(rqptr), WATCH_AUTH, "TOKEN !AZ for !ULS",
                 TokenValue, TokenLifeTime);

   InstanceMutexLock (INSTANCE_MUTEX_AUTH_TOKEN);

   /* if a previous record is not located then find one to (re)use */
   if (!(atkptr = AuthTokenFind (TokenValuePtr)))
      atkptr = AuthTokenFind (NULL);

   if (atkptr)
   {
      /* (refresh) using the new token value */
      strcpy (atkptr->TokenValue, TokenValue);
      strcpy (atkptr->AuthRealm, rqptr->rqAuth.RealmPtr);
      strcpy (atkptr->RemoteUser, rqptr->RemoteUser);
      atkptr->RemoteUserLength = rqptr->RemoteUserLength;
      atkptr->SysUafAuthenticated = rqptr->rqAuth.SysUafAuthenticated;
      atkptr->VmsUserProfile = (rqptr->rqAuth.VmsUserProfilePtr != NULL);
      atkptr->AuthUserCan = rqptr->rqAuth.UserCan;
      atkptr->ExpireSecond = HttpdTickSecond + TokenLifeTime;
      IPADDRESS_COPY (&atkptr->ClientIpAddr, &rqptr->ClientPtr->IpAddress);
   }

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_TOKEN);

   if (!atkptr)
   {
      FaoToStdout (
"%HTTPD-W-AUTHTOKEN, !20%D, global section exhausted at !UL records\n",
                   AuthTokenRecordMax);

      if (OpcomMessages & OPCOM_AUTHORIZATION)
         FaoToOpcom (
"%HTTPD-W-AUTHTOKEN, global section exhausted\r\n",
                     AuthTokenRecordMax);

      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   /*******************/
   /* generate cookie */
   /*******************/

   SemiColon = rqptr->rqHeader.UserAgentPtr &&
               !strncmp (rqptr->rqHeader.UserAgentPtr, "Opera/", 6);

   CookieSize = TokenNameLength + 3 + AUTH_SIZE_TOKEN_VALUE;
   CookieSize += sizeof(" domain=\"\"") + rqptr->rqHeader.HostLength;
   CookieSize += sizeof(" path=\"\"") + rqptr->rqHeader.RequestUriLength;
   CookieSize += sizeof(" max-age=\"\"") + TokenMaxAgeLength;
   if (SemiColon) CookieSize += 3;
   CookiePtr = VmGetHeap (rqptr, CookieSize);

   zptr = (sptr = CookiePtr) + CookieSize;
   for (cptr = TokenName;
        TokenNameLength-- && sptr < zptr;
        *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '=';
   if (sptr < zptr) *sptr++ = '\"';
   for (cptr = TokenValue; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '\"';
   if (SemiColon && sptr < zptr) *sptr++ = ';';
   for (cptr = " domain=\""; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = rqptr->rqHeader.HostPtr;
        *cptr && sptr < zptr;
        *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '\"';
   if (SemiColon && sptr < zptr) *sptr++ = ';';
   for (cptr = " path=\""; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = rqptr->rqHeader.RequestUriPtr;
        *cptr && sptr < zptr;
        *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '\"';
   if (SemiColon && sptr < zptr) *sptr++ = ';';
   for (cptr = " max-age=\""; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = TokenMaxAgePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '\"';
   *sptr = '\0';

   for (idx = 0; idx < RESPONSE_COOKIE_MAX; idx++)
   {
      if (!rqptr->rqResponse.CookiePtr[idx])
      {
         rqptr->rqResponse.CookiePtr[idx] = CookiePtr;
         break;
      }
   }
   if (idx >= RESPONSE_COOKIE_MAX)
   {
      if (WATCHING (rqptr, WATCH_AUTH))
         WatchThis (WATCHITM(rqptr), WATCH_AUTH, "cookie storage exhausted");
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }
 
   /************/
   /* redirect */
   /************/

   LocationSize = rqptr->rqHeader.HostLength + TokenPortLength +
                  rqptr->rqHeader.RequestUriLength + 11;
   /* reserves space in the dictionary that will then be populated */
   sptr = ResponseLocation (rqptr, NULL, LocationSize);
   zptr = sptr + LocationSize;

   for (cptr = SchemePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = '/';
   if (sptr < zptr) *sptr++ = '/';
   for (cptr = rqptr->rqHeader.HostPtr;
        *cptr && *cptr != ':' && sptr < zptr;
        *sptr++ = *cptr++);

   if (TokenPortPtr)
   {
      if (sptr < zptr) *sptr++ = ':';
      for (cptr = TokenPortPtr;
           TokenPortLength-- && sptr < zptr; 
           *sptr++ = *cptr++);
   }

   for (cptr = rqptr->rqHeader.RequestUriPtr;
        *cptr && sptr < zptr;
        *sptr++ = *cptr++);
   *sptr = '\0';

   rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_REDIRECT;
}

/****************************************************************************/
/*
Authorisation by token is occurring (commonly in non-SSL space).  It looks for
a token delivered in a cookie.  If present it is searched for as a correspnding
record in the global section.  If found the expiry time and client IP address
are checked.  If after that all is OK the access is permitted.  If any of this
fails the request is redirected to the SSL-space equivalent where
authentication can be applied securely and from that generate the token and
associated cookie.
*/ 

void AuthTokenProcess (REQUEST_STRUCT* rqptr)

{
   BOOL  IPaddrMismatch = false,
         RealmMismatch = false,
         VmsUserProfile = false;
   int  ExpiresSecs = 0,
        LocationSize,
        TokenIntegerLength = 0,
        TokenNameLength,
        TokenPortLength = 0;
   char  *cptr, *sptr, *zptr,
         *SchemePtr = "https:",
         *TokenIntegerPtr = NULL,
         *TokenNamePtr = NULL,
         *TokenPortPtr = NULL,
         *TokenValuePtr;
   char  TokenName [64],
         TokenValue [AUTH_SIZE_TOKEN_VALUE+1];
   AUTH_TOKEN_RECORD  *atkptr = NULL;

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

   if (WATCHMOD (rqptr, WATCH_MOD_AUTH))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH,
                 "AuthTokenProcess() !&Z", rqptr->rqHeader.CookiePtr);

   if (!AuthTokenRecordMax)
   {
      if (WATCHING (rqptr, WATCH_AUTH))
          WatchThis (WATCHITM(rqptr), WATCH_AUTH, "TOKEN disabled 0 entries");
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   if (!rqptr->rqHeader.HostPtr ||
       !rqptr->rqHeader.RequestUriPtr)
   {
      ErrorNoticed (rqptr, SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
      return;
   }

   if (rqptr->rqAuth.RealmParamPtr &&
       *(cptr = rqptr->rqAuth.RealmParamPtr))
   {
      if (strsame (cptr, "TOKEN=", 6))
      {
         /**************/
         /* parameters */
         /**************/

         cptr += 6;
         while (*cptr)
         {
            if (*cptr == '*')
               cptr++;
            else
            if (*cptr == ':' && isdigit(*cptr+1))
            {
               TokenPortPtr = ++cptr;
               while (isdigit(*cptr)) cptr++;
               TokenPortLength = cptr - TokenPortPtr;
            }
            else
            if (*cptr == '&' && isdigit(*(cptr+1)))
            {
               TokenIntegerPtr = ++cptr;
               while (isdigit(*cptr)) cptr++;
               TokenIntegerLength = cptr - TokenIntegerPtr;
            }
            else
            if (strsame (cptr, "http:", 5))
            {
               SchemePtr = "http:";
               cptr += 5;
               if (isdigit(*cptr)) cptr--;
            }
            else
            if (strsame (cptr, "https:", 6))
            {
               SchemePtr = "https:";
               cptr += 6;
               if (isdigit(*cptr)) cptr--;
            }
            else
            if (isalpha(*cptr))
            {
               for (TokenNamePtr = cptr; *cptr && isalpha(*cptr); cptr++);
               TokenNameLength = cptr - TokenNamePtr;
            }
            else
               while (*cptr && !(ISLWS(*cptr) || *cptr == ',')) cptr++;

            while (*cptr && (ISLWS(*cptr) || *cptr == ',')) cptr++;
         }
      }
   }

   if (!TokenNamePtr)
   {
      TokenNamePtr = AUTH_TOKEN_NAME_DEFAULT;
      TokenNameLength = sizeof(AUTH_TOKEN_NAME_DEFAULT)-1;
   }
   zptr = (sptr = TokenName) + sizeof(TokenName)-1;
   for (cptr = TokenNamePtr;
        TokenNameLength-- && sptr < zptr;
        *sptr++ = *cptr++);
   if (TokenIntegerLength)
      for (cptr = TokenIntegerPtr;
           TokenIntegerLength-- && sptr < zptr;
           *sptr++ = *cptr++);
   *sptr = '\0';

   TokenValuePtr = AuthTokenFromCookie (rqptr, TokenName);

   if (TokenValuePtr)
   {
      /****************/
      /* verify token */
      /****************/

      if (WATCHING (rqptr, WATCH_AUTH))
         WatchThis (WATCHITM(rqptr), WATCH_AUTH, "TOKEN !AZ", TokenValuePtr);

      InstanceMutexLock (INSTANCE_MUTEX_AUTH_TOKEN);

      atkptr = AuthTokenFind (TokenValuePtr);

      if (atkptr)
      {
         /* surely this could only happen subversively! */
         if (!IPADDRESS_IS_SAME(&atkptr->ClientIpAddr,
                                &rqptr->ClientPtr->IpAddress))
         {
            atkptr = NULL;
            IPaddrMismatch = true;
         }
      }

      if (atkptr)
      {
         /* compare the request and record realms */
         if (!strsame (rqptr->rqAuth.RealmPtr, atkptr->AuthRealm, -1))
         {
            /* can just be misconfiguration */
            atkptr = NULL;
            RealmMismatch = true;
         }
      }

      if (atkptr)
      {
         /* check the token has not expired */
         ExpiresSecs = atkptr->ExpireSecond - (unsigned int)HttpdTickSecond;
         if (ExpiresSecs < 0) atkptr = NULL;
      }

      if (atkptr)
      {
         /* validated so use the record */
         strcpy (rqptr->RemoteUser, atkptr->RemoteUser);
         rqptr->RemoteUserLength = atkptr->RemoteUserLength;
         rqptr->rqAuth.UserCan = atkptr->AuthUserCan;
         rqptr->rqAuth.SysUafAuthenticated = atkptr->SysUafAuthenticated;
         VmsUserProfile = atkptr->VmsUserProfile;
      }

      InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_TOKEN);

      if (IPaddrMismatch)
      {
         if (WATCHING (rqptr, WATCH_AUTH))
            WatchThis (WATCHITM(rqptr), WATCH_AUTH, "IP address MISMATCH");

         FaoToStdout (
"%HTTPD-W-AUTHTOKEN, !20%D, IP address mismatch\n\
-AUTHTOKEN-I-SERVICE, !AZ//!AZ\n\
-AUTHTOKEN-I-CLIENT, !AZ\n\
-AUTHTOKEN-I-URI, !AZ !AZ\n",
            0, rqptr->ServicePtr->RequestSchemeNamePtr,
            rqptr->ServicePtr->ServerHostPort,
            ClientHostString(rqptr),
            rqptr->rqHeader.MethodName, rqptr->rqHeader.RequestUriPtr);

         if (OpcomMessages & OPCOM_AUTHORIZATION)
            FaoToOpcom (
"%HTTPD-W-AUTHTOKEN, IP address mismatch\r\n\
-AUTHTOKEN-I-SERVICE, !AZ//!AZ\r\n\
-AUTHTOKEN-I-CLIENT, !AZ\r\n\
-AUTHTOKEN-I-URI, !AZ !AZ",
               rqptr->ServicePtr->RequestSchemeNamePtr,
               rqptr->ServicePtr->ServerHostPort,
               ClientHostString(rqptr),
               rqptr->rqHeader.MethodName, rqptr->rqHeader.RequestUriPtr);

         rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
         return;
      }

      if (RealmMismatch)
      {
         if (WATCHING (rqptr, WATCH_AUTH))
            WatchThis (WATCHITM(rqptr), WATCH_AUTH, "REALM MISMATCH");

         rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_OTHER;
         return;
      }

      if (WATCHING (rqptr, WATCH_AUTH))
         WatchThis (WATCHITM(rqptr), WATCH_AUTH, "EXPIRES !AZ!SLS",
                    ExpiresSecs >= 0 ? "+" : "", ExpiresSecs);
   }

   if (TokenValuePtr && atkptr)
   {
      /************/
      /* verified */
      /************/

      rqptr->rqAuth.ResolvedRemoteUser = true;
      rqptr->rqAuth.FinalStatus = SS$_NORMAL;
      if (rqptr->rqAuth.SysUafAuthenticated)
      {
         strcpy (rqptr->rqAuth.RemoteUser, rqptr->RemoteUser);
         rqptr->rqAuth.RemoteUserLength = rqptr->RemoteUserLength;
         if (VmsUserProfile)
            rqptr->rqAuth.FinalStatus = AuthVmsCreateUserProfile (rqptr);
      }
   }
   else
   {
      /************/
      /* redirect */
      /************/

      LocationSize = rqptr->rqHeader.HostLength + TokenPortLength +
                     rqptr->rqHeader.RequestUriLength + 11;
      /* reserves space in the dictionary that will then be populated */
      sptr = ResponseLocation (rqptr, NULL, LocationSize);
      zptr = sptr + LocationSize;

      for (cptr = SchemePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr < zptr) *sptr++ = '/';
      if (sptr < zptr) *sptr++ = '/';
      for (cptr = rqptr->rqHeader.HostPtr;
           *cptr && *cptr != ':' && sptr < zptr;
           *sptr++ = *cptr++);

      if (TokenPortPtr)
      {
         if (sptr < zptr) *sptr++ = ':';
         for (cptr = TokenPortPtr;
              TokenPortLength-- && sptr < zptr; 
              *sptr++ = *cptr++);
      }

      for (cptr = rqptr->rqHeader.RequestUriPtr;
           *cptr && sptr < zptr;
           *sptr++ = *cptr++);
      *sptr = '\0';

      rqptr->rqAuth.FinalStatus = AUTH_DENIED_BY_REDIRECT;
   }
}

/*****************************************************************************/
/*
Find and return the token value from the cookie.  Return NULL if not found.
*/ 

char* AuthTokenFromCookie
(
REQUEST_STRUCT* rqptr,
char *TokenNamePtr
)
{
   char  *cptr, *sptr, *zptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_AUTH))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_AUTH,
                 "AuthTokenFromCookie() !&Z", TokenNamePtr);

   if (!rqptr->rqHeader.CookiePtr) return (NULL);

   if (cptr = strstr (rqptr->rqHeader.CookiePtr, TokenNamePtr))
   {
      cptr += strlen(TokenNamePtr);
      if (*cptr++ == '=')
         if (*cptr++ == '\"')
         {
            sptr = VmGetHeap (rqptr, AUTH_SIZE_TOKEN_VALUE+1);
            zptr = (rqptr->rqAuth.AuthTokenValuePtr = sptr) +
                   AUTH_SIZE_TOKEN_VALUE;
            while (*cptr && *cptr != '\"' && sptr < zptr) *sptr++ = *cptr++;
            if (sptr == zptr && *cptr == '\"')
            {
               *sptr = '\0';
               return (rqptr->rqAuth.AuthTokenValuePtr);
            }
         }
   }

   return (NULL);
}

/*****************************************************************************/
/*
Expects the global section to be locked.  If token is NULL then returns the
first available record for (re)use.  Returns NULL if not found or usable
records are exhausted.
*/ 

AUTH_TOKEN_RECORD* AuthTokenFind (char *TokenValue)

{
   int  idx;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH,
                 "AuthTokenFind() !&Z in !UL of !UL records",
                 TokenValue, AuthTokenGblSecPtr->RecordCount,
                 AuthTokenRecordMax);

   if (TokenValue)
   {
      /* find the specified record */
      for (idx = 0; idx < AuthTokenGblSecPtr->RecordCount; idx++)
      {
         if (WATCH_MODULE(WATCH_MOD_AUTH))
            WatchThis (WATCHALL, WATCH_MOD_AUTH,
                       "!&Z", AuthTokenDataBase[idx].TokenValue);
         if (*(ULONGPTR)(AuthTokenDataBase[idx].TokenValue) !=
             *(ULONGPTR)TokenValue) continue;
         if (!strcmp (AuthTokenDataBase[idx].TokenValue, TokenValue))
            return (&AuthTokenDataBase[idx]);
      }
   }
   else
   {
      /* find a record that can be (re)used */
      for (idx = 0; idx < AuthTokenRecordMax; idx++)
      {
         if (WATCH_MODULE(WATCH_MOD_AUTH))
            WatchThis (WATCHALL, WATCH_MOD_AUTH,
                       "!&Z", AuthTokenDataBase[idx].TokenValue);
         if (!AuthTokenDataBase[idx].TokenValue[0])
         {
            AuthTokenGblSecPtr->RecordCount++;
            return (&AuthTokenDataBase[idx]);
         }
         if (AuthTokenDataBase[idx].ExpireSecond < HttpdTickSecond)
            return (&AuthTokenDataBase[idx]);
      }
   }

   return (NULL);
}

/*****************************************************************************/
/*
If only one instance can execute (from configuration) then allocate a block of
process-local dynamic memory and point to that as the cache.  If multiple
instances create and map a global section and point to that.  This is not
permananent and so requires no deletion function.
*/ 

AuthTokenGblSecInit ()

{
   static char  GblSecReport [] =
"%HTTPD-I-AUTHTOKEN, for !UL records in !AZ of !UL page(let)s\n";

   /* global, allocate space, system, in page file, writable */
   static int CreFlags = SEC$M_GBL | SEC$M_EXPREG | SEC$M_SYSGBL |
                         SEC$M_PAGFIL | SEC$M_WRT;
   static int DelFlags = SEC$M_SYSGBL;
   /* system & owner full access, group and world no access */
   static unsigned long  ProtectionMask = 0xff00;
   /* it is recommended to map into any virtual address in the region (P0) */
   static unsigned long  InAddr [2] = { 0x200, 0x200 };

   int  attempt, status,
        BaseGblSecPages,
        TokenRecordPoolSize,
        PageCount,
        SetPrvStatus;
   short  ShortLength;
   unsigned long  RetAddr [2];
   char  GblSecName [32];
   $DESCRIPTOR (GblSecNameDsc, GblSecName);
   AUTH_TOKEN_GBLSEC  *gsptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH, "AuthTokenGblSecInit()");

   TokenRecordPoolSize = AuthTokenRecordSize * AuthTokenRecordMax;
   AuthTokenGblSecSize = sizeof(AUTH_TOKEN_GBLSEC) + TokenRecordPoolSize;
   AuthTokenGblSecPages = AuthTokenGblSecSize / 512;
   if (AuthTokenGblSecSize & 0x1ff) AuthTokenGblSecPages++;

   if (InstanceNodeConfig <= 1)
   {
      /* no need for a global section, just use process-local storage */
      AuthTokenGblSecPtr =
         (AUTH_TOKEN_GBLSEC*)VmGet (AuthTokenGblSecPages * 512);
      sys$gettim (&AuthTokenGblSecPtr->SinceTime64);
      FaoToStdout (GblSecReport, AuthTokenRecordMax,
                   "local storage", AuthTokenGblSecPages);
      return (SS$_CREATED);
   }

   FaoToBuffer (GblSecName, sizeof(GblSecName), &ShortLength,
                GBLSEC_NAME_FAO, HTTPD_NAME, AUTH_TOKEN_GBLSEC_VERSION_NUMBER,
                InstanceEnvNumber, "AUTH_TOKEN");
   GblSecNameDsc.dsc$w_length = ShortLength;

   if VMSnok ((SetPrvStatus = sys$setprv (1, &GblSecPrvMask, 0, 0)))
      ErrorExitVmsStatus (SetPrvStatus, "sys$setprv()", FI_LI);

   for (attempt = 1; attempt <= 2; attempt++)
   {
      /* create and/or map the specified global section */
      sys$setprv (1, &GblSecPrvMask, 0, 0);
      status = sys$crmpsc (&InAddr, &RetAddr, 0, CreFlags,
                           &GblSecNameDsc, 0, 0, 0, AuthTokenGblSecPages, 0,
                           ProtectionMask, AuthTokenGblSecPages);
      sys$setprv (0, &GblSecPrvMask, 0, 0);

      if (WATCH_MODULE(WATCH_MOD_AUTH))
         WatchThis (WATCHALL, WATCH_MOD_AUTH,
                    "sys$crmpsc() !&S begin:!UL end:!UL",
                    status, RetAddr[0], RetAddr[1]);

      PageCount = (RetAddr[1]+1) - RetAddr[0] >> 9;
      AuthTokenGblSecPtr = gsptr = (AUTH_TOKEN_GBLSEC*)RetAddr[0];
      AuthTokenGblSecPages = PageCount;
      if (VMSnok (status) || status == SS$_CREATED) break;

      /* section already exists, break if 'same size' and version! */
      if (gsptr->GblSecVersion &&
          gsptr->GblSecVersion == AuthTokenGblSecVersion &&
          gsptr->GblSecLength == AuthTokenGblSecSize)
         break;

      /* delete the current global section, have one more attempt */
      sys$setprv (1, &GblSecPrvMask, 0, 0);
      status = sys$dgblsc (DelFlags, &GblSecNameDsc, 0);
      sys$setprv (0, &GblSecPrvMask, 0, 0);
      status = SS$_IDMISMATCH;
   }

   if (VMSnok (status))
   {
      /* must have this global section! */
      char  String [256];
      FaoToBuffer (String, sizeof(String), NULL,
                   "1 global section, !UL global pages",
                   AuthTokenGblSecPages);
      ErrorExitVmsStatus (status, String, FI_LI);
   }

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH,
         "GBLSEC \"!AZ\" page(let)s:!UL !&S",
         GblSecName, PageCount, status);

   FaoToStdout (GblSecReport, AuthTokenRecordMax,
                status == SS$_CREATED ? "a new global section" :
                                        "an existing global section",
                AuthTokenGblSecPages);

   if (status == SS$_CREATED)
   {
      /* first time it's been mapped */
      memset (gsptr, 0, PageCount * 512);
      gsptr->GblSecVersion = AuthTokenGblSecVersion;
      gsptr->GblSecLength =  AuthTokenGblSecSize;
      sys$gettim (&AuthTokenGblSecPtr->SinceTime64);
   }

   GblSectionCount++;
   GblPageCount += PageCount;

   return (status);
}

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

