/*****************************************************************************/
/*
                               Sesola[A]cme.c

Activate a CGI agent to service this particular Application-Layer Protocol
Negotiation.  Initially intended for ACME's alpn-tls-01 protocol

  https://letsencrypt.org/docs/challenge-types/
  https://datatracker.ietf.org/doc/html/rfc7301

This functionality is activated from SesolaHelloCallback().

Simple test case:

openssl s_client -debug -connect <host>:443 -servername <host> -alpn acme-tls/1

Processing can be WATCHed using [x]CGI and [x]DCL along with a *special case* 
RX trigger of "acme-tls/1".


VERSION HISTORY
---------------
16-SEP-2024  MGD  process a configured ALPN response as a DCL script
*/
/*****************************************************************************/

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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

/* application header files */
#define SESOLA_REQUIRED
#include "Sesola.h"

#define WASD_MODULE "SESOLACME"

/***************************************/
#ifdef SESOLA  /* secure sockets layer */
/***************************************/

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

char  ProblemAlpnAgent [] = "Problem initiating ALPN agent";

#define ALPN_AGENT_ACTIVE_MAX 8
#define ALPN_AGENT_BUSY_MAX 100
static int  SesolaAcmeAgentActiveCount,
            SesolaAcmeAgentActiveMax = ALPN_AGENT_ACTIVE_MAX,
            SesolaAcmeAgentBusyMax = ALPN_AGENT_BUSY_MAX,
            SesolaAcmeAgentBusyCount,
            SesolaAcmeAgentBusyLimit;

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

extern char  SoftwareID[];

extern const int64  Delta100mSec;

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

/*****************************************************************************/
/*
This function is called during the early stages of client hello processing on
the server to determine whether the ALPN is TLS-ALPN-01 ("acme-tls/1").
https://datatracker.ietf.org/doc/html/rfc8737
*/

int SesolaAcmeTls1
(
SSL *SslPtr,
int *al,
void *arg
)
{
   int  nlen, retval, status, extLen;
   char  *cptr, *czptr, *sptr, *zptr;
   unsigned char  *extData;
   SESOLA_STRUCT  *sesolaptr;

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

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeTls1()");

   sesolaptr = (SESOLA_STRUCT*)SSL_get_ex_data (SslPtr, 0);

   /* if already discovered the protocol */
   if (sesolaptr->AlpnProtocol) return (SSL_CLIENT_HELLO_SUCCESS);

   /* get the ALPN */
   retval = SSL_client_hello_get0_ext (SslPtr,
                TLSEXT_TYPE_application_layer_protocol_negotiation,
                &extData,
                &extLen);

   if (retval != 1) return (SSL_CLIENT_HELLO_SUCCESS);

   czptr = (cptr = extData) + extLen;
   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchDataDump (cptr, czptr - cptr);
   cptr += sizeof(ushort);
   /* provide an ALPN extension with the single protocol name "acme-tls/1" */
   if (cptr < czptr)
   {
      nlen = (uint)(uchar*)*cptr++;
      if (WATCH_MODULE(WATCH_MOD_SESOLA))
         WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL !#AZ",
                    nlen, nlen, cptr);
      if (!strncmp (cptr, "acme-tls/1", nlen))
         sesolaptr->AlpnProtocol = "acme-tls/1";
      cptr += nlen;
   }
   /* if not acme-tls/1 or not alone */
   if (!sesolaptr->AlpnProtocol || cptr < czptr)
      return (SSL_CLIENT_HELLO_SUCCESS);

   /* continue on to the get server name */
   retval = SSL_client_hello_get0_ext (SslPtr,
                TLSEXT_TYPE_server_name,
                &extData,
                &extLen);

   if (retval != 1) return (SSL_CLIENT_HELLO_ERROR);

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL", extLen);

   czptr = (cptr = extData) + extLen;
   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchDataDump (cptr, czptr - cptr);
   cptr += sizeof(ushort);
   if (cptr >= czptr) return (SSL_CLIENT_HELLO_ERROR);
   if (*(USHORTPTR)cptr != TLSEXT_NAMETYPE_host_name)
      return (SSL_CLIENT_HELLO_ERROR);
   cptr += sizeof(ushort);
   if (cptr < czptr)
   {
      nlen = (uint)(uchar*)*cptr++;
      if (WATCH_MODULE(WATCH_MOD_SESOLA))
         WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL !#AZ",
                    nlen, nlen, cptr);
      zptr = (sptr = sesolaptr->SNIServerName) +
             sizeof(sesolaptr->SNIServerName)-1;
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
      if (WATCH_MODULE(WATCH_MOD_SESOLA))
         WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!AZ",
                    sesolaptr->SNIServerName);
   }
   /* if acme-tls/1 with no server name */
   if (!sesolaptr->SNIServerName[0]) return (SSL_CLIENT_HELLO_ERROR);

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!AZ !AZ",
                 sesolaptr->AlpnProtocol, sesolaptr->SNIServerName);

   /* set up the agent's operating parameters */
   sesolaptr->AlpnAgentAstFunction = SesolaDassgnChannel;
   sesolaptr->AlpnAgentAstParam = sesolaptr;

   status = SesolaAcmeAgentBegin (sesolaptr);
   if (VMSnok (status))
   {
      if (WATCHMOD (sesolaptr, WATCH_MOD_SESOLA))
          WatchThis (WATCHITM(sesolaptr), WATCH_MOD_SESOLA,
                     "SesolaAcmeAgentBegin() !&S", status);
      return (SSL_CLIENT_HELLO_ERROR);
   }

   return (SSL_CLIENT_HELLO_RETRY);
}

/*****************************************************************************/
/*
Activate an agent (script) to perform the the ACME certificate generation used 
used, in this case by wuCME.

ASYNCHRONOUS!

An ACME agent does not have an associated request, it creates a faux one!
*/

int SesolaAcmeAgentBegin (void *vptr)

{
   int  status;
   char  *cptr, *sptr, *zptr;
   char  AgentScript [128];
   REQUEST_STRUCT  *rqptr;
   SESOLA_STRUCT  *sesolaptr;

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

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentBegin()");

   sesolaptr = (SESOLA_STRUCT*)vptr;

   if (!sesolaptr->AlpnAgentAstFunction)
   {
      /* no AST supplied then an error */
      return (SS$_ABORT);
   }

   if (SesolaAcmeAgentActiveCount >= SesolaAcmeAgentActiveMax)
   {
      /* busy, busy, busy ... requeue for a delayed subsequent attempt */
      if (SesolaAcmeAgentBusyCount++ <= SesolaAcmeAgentBusyMax)
      {
         if (SesolaAcmeAgentBusyCount <= SesolaAcmeAgentBusyMax / 4)
            SysDclAst (SesolaAcmeAgentBegin, sesolaptr);
         else
            sys$setimr (0, &Delta100mSec, SesolaAcmeAgentBegin, sesolaptr, 0);
         return (SS$_NORMAL);
      }
      else
      {
         ErrorNoticed (NULL, SS$_TOOMANYREDS, "!UL exceeded (!UL)",
                       FI_LI, SesolaAcmeAgentBusyMax, SesolaAcmeAgentBusyLimit);
         SesolaAcmeAgentBusyCount++;
         sesolaptr->AlpnAgentStatus = SS$_ABORT;
         SysDclAst (sesolaptr->AlpnAgentAstFunction,
                    sesolaptr->AlpnAgentAstParam);
         return (SS$_ABORT);
      }
   }
   if (SesolaAcmeAgentBusyCount)
   {
      if (SesolaAcmeAgentBusyCount > SesolaAcmeAgentBusyLimit)
         SesolaAcmeAgentBusyLimit = SesolaAcmeAgentBusyCount;
      SesolaAcmeAgentBusyCount = 0;
   }

   /************/
   /* continue */
   /************/

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "begin !AZ",
                 sesolaptr->AlpnProtocol);

   rqptr = DclFauxRequest (NULL, NULL, NULL);

   rqptr->AgentAstParam = (void*)sesolaptr;

   rqptr->AgentRequestPtr = VmGetHeap (rqptr, 256);
   zptr = (sptr = rqptr->AgentRequestPtr) + 255;

   for (cptr = sesolaptr->SNIServerName;
        *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr < zptr) *sptr++ = ' ';

   /* e.g. "acme-tls/1" becomes "/cgi-bin/acme_tls_1" */
   zptr = (sptr = AgentScript) + sizeof(AgentScript)-1;
   for (cptr = "/cgi-bin/"; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = sesolaptr->AlpnProtocol; *cptr && sptr < zptr; cptr++)
      if (isalnum (*cptr))
         *sptr++ = *cptr;
      else
         *sptr++ = '_';
   *sptr = '\0';

   rqptr->WatchItem = sesolaptr->NetIoPtr->WatchItem;

   if (Watch.TriggerRxCount)
      if (!strcmp (Watch.TriggerRx, sesolaptr->AlpnProtocol))
         rqptr->WatchItem = ++Watch.ItemCount;

   if (WATCHING (rqptr, WATCH_SESOLA))
      WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "ACME !AZ AGENT !AZ",
                 sesolaptr->AlpnProtocol, AgentScript);

   rqptr = DclFauxRequest (rqptr, AgentScript, &SesolaAcmeAgentEnd);

   if (!rqptr)
   {
      ErrorNoticed (NULL, SS$_ABORT, ProblemAlpnAgent, FI_LI);
      sesolaptr->AlpnAgentStatus = SS$_ABORT;
      SysDclAst (sesolaptr->AlpnAgentAstFunction, sesolaptr->AlpnAgentAstParam);
      return (SS$_ABORT);
   }

   SesolaAcmeAgentActiveCount++;

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
The ACME agent (script) has completed.

Should have returned OPAQUE: data and then copied into ACME storage, with a
format <ushort-length><crt-data><ushort-length><key-data>.

Continue the TLS handshake.
*/

void SesolaAcmeAgentEnd (REQUEST_STRUCT *rqptr)

{
   int  count, line, retval, status;
   char  *aptr, *cptr;
   SESOLA_STRUCT  *sesolaptr;
   SSL  *SslPtr;

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

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA,
                "SesolaAcmeAgentEnd() 0x!8XL !UL",
                 rqptr->OpaqueDataPtr, rqptr->OpaqueDataCount);

   sesolaptr = (SESOLA_STRUCT*)rqptr->AgentAstParam;

   SslPtr = sesolaptr->SslPtr;

   sesolaptr->AlpnAgentStatus = SS$_NORMAL;

   if (cptr = rqptr->AgentResponsePtr)
   {
      if (WATCHING (rqptr, WATCH_SESOLA))
         WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "ACME !AZ", cptr);

      status = atoi(cptr);
      while (isdigit (*cptr)) cptr++;
      while (*cptr == ' ') cptr++;

      if (status >= 200 && status <= 299)
      {
         /***********/
         /* success */
         /***********/

         if (rqptr->OpaqueDataPtr)
         {
            sesolaptr->AcmeDataPtr = VmGet (rqptr->OpaqueDataCount);
            memcpy (sesolaptr->AcmeDataPtr,
                    rqptr->OpaqueDataPtr,
                    rqptr->OpaqueDataCount);
            sesolaptr->AcmeDataCount = rqptr->OpaqueDataCount;
            sesolaptr->AlpnAgentStatus = SS$_NORMAL;
            line = __LINE__;
         }
         else
         {
            sesolaptr->AlpnAgentStatus = SS$_ABORT;
            line = __LINE__;
         }
      }
      else
      {
         /**********/
         /* failed */
         /**********/

         while (*cptr && *(USHORTPTR)cptr != '%X') cptr++;
         if (*cptr)
         {
            /* if agent reporting VMS error */
            status = strtol (cptr+2, NULL, 16);
            sesolaptr->AlpnAgentStatus = status;
            line = __LINE__;
         }
         else
         {
            sesolaptr->AlpnAgentStatus = SS$_ABORT;
            line = __LINE__;
         }
      }
   }
   else
   {
      sesolaptr->AlpnAgentStatus = SS$_ABORT;
      line = __LINE__;
   }

   if (SesolaAcmeAgentActiveCount) SesolaAcmeAgentActiveCount--;

   if (WATCHING (rqptr, WATCH_SESOLA))
      WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "ACME !AZ !&S",
                 sesolaptr->AlpnProtocol, sesolaptr->AlpnAgentStatus);

   SysDclAst (sesolaptr->AlpnAgentAstFunction, sesolaptr->AlpnAgentAstParam);

   if (VMSok (sesolaptr->AlpnAgentStatus))
   {
      retval = SSL_do_handshake (SslPtr);
      if (retval <= 0)
      {
         sesolaptr->AlpnAgentStatus = SS$_ABORT;
         line = __LINE__;
      }
   }

   ErrorNoticed (NULL, sesolaptr->AlpnAgentStatus, "\"!AZ\" !AZ !AZ",
                 WASD_MODULE, line,
                 rqptr->AgentResponsePtr,
                 sesolaptr->SNIServerName,
                 sesolaptr->NetIoPtr->ClientPtr->Lookup.HostName);

   /* remove from any supervisory list */
   HttpdSupervisorList (rqptr, -1);

   VmFreeRequest (rqptr, FI_LI);

   /* "close" the SSL connection */
   SesolaNetBeginFail (sesolaptr);
}

/*****************************************************************************/
/*
The agent (script) should have returned OPAQUE: data and then copied into ACME
storage, with a format <ushort-length><crt-data><ushort-length><key-data>.

The data is reconstructed into certificate and private key and then set for
this connection for when the handshake is finalised.

Called from SesolaAlpnCallback().
*/

int SesolaAcmeCertify (SESOLA_STRUCT *sesolaptr)

{
   int  crtlen, keylen, retval, status;
   char  *bptr, *bzptr, *crtptr, *keyptr;
   SSL  *SslPtr;

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

   if (WATCH_MODULE (WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA,
                 "SesolaAcmeCertify() 0x!8XL !UL",
                 sesolaptr->AcmeDataPtr, sesolaptr->AcmeDataCount);

   SslPtr = sesolaptr->SslPtr;

   if (!sesolaptr->AcmeDataPtr) return (SS$_BADPARAM);

   crtptr = keyptr = NULL;
   crtlen = keylen = 0;

   bzptr = (bptr = sesolaptr->AcmeDataPtr) + sesolaptr->AcmeDataCount;
   if (bptr + 2 < bzptr)
   {
      /* get certificate length and certificate itself */
      crtlen = *(USHORTPTR)bptr;
      bptr += 2;
      if (bptr + crtlen < bzptr)
      {
         crtptr = bptr;
         bptr += crtlen;
      }
      if (bptr + 2 < bzptr)
      {
         /* get key length and key itself */
         keylen = *(USHORTPTR)bptr;
         bptr += 2;
         if (bptr + keylen <= bzptr)
            keyptr = bptr;
      }
   }
   
   status = SS$_NORMAL;
   if (crtlen + keylen + sizeof(ushort) + sizeof(ushort) !=
       sesolaptr->AcmeDataCount)
   {
      if (WATCH_MODULE (WATCH_MOD_SESOLA))
         WatchThis (WATCHALL, WATCH_SESOLA, "ACME sanity !UL !UL !UL",
                    sesolaptr->AcmeDataCount, crtlen, keylen);
      status = SS$_BADPARAM;
   }
   if (!crtptr || !keyptr)
   {
      if (WATCH_MODULE (WATCH_MOD_SESOLA))
         WatchThis (WATCHALL, WATCH_SESOLA, "ACME sanity 0x!8XL 0x!8XL",
                    crtptr, keyptr);
      status = SS$_BADPARAM;
   }

   if (VMSok (status))
   {
      retval = SSL_use_certificate_ASN1 (SslPtr, crtptr, crtlen);
      if (!retval)
      {
         if (WATCH_MODULE (WATCH_MOD_SESOLA))
            WatchThis (WATCHALL, WATCH_SESOLA, "ACME cert failed");
         status = SS$_ABORT;
      }
   }

   if (VMSok (status))
   {
      retval = SSL_use_PrivateKey_ASN1(EVP_PKEY_EC, SslPtr, keyptr, keylen);
      if (!retval)
      {
         if (WATCH_MODULE (WATCH_MOD_SESOLA))
            WatchThis (WATCHALL, WATCH_SESOLA, "ACME key failed");
         status = SS$_ABORT;
      }
   }

   return (status);
}

/*****************************************************************************/
/*
For compilations without SSL these functions provide LINKage stubs for the
rest of the HTTPd modules, allowing for just recompiling the Sesola module to
integrate the SSL functionality.
*/

/*********************/
#else  /* not SESOLA */
/*********************/

/* external storage */
extern WATCH_STRUCT  Watch;

int SesolaAcmeAgentBegin (void *vptr)
{
   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentBegin()");
   return (SS$_ABORT);
}

void SesolaAcmeAgentEnd (REQUEST_STRUCT *rqptr)
{
   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentEnd()");
}

void SesolaAcmeCertify (void *ptr)
{
   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeCertify()");
}

/************************/
#endif  /* ifdef SESOLA */
/************************/

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

