/*****************************************************************************/
/*
                                 CSPreport.c

Content Security Policy Report[er]

  https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

When POSTed to, this utility appends a timestamp and CSP report JSON to the
file specified by the CSPREPORT_FILE logical name.  This file must be located
somewhere the scripting account has read+write access to.  When accessed using
a GET the utility accesses the stored CSP reports and returns a formatted HTML
report listing each.  GET requests (reporting) must be subject to
authentication and authorisation.

WARNING: depending on the CSP options selected and applied a busy site can
generate *LOTS* of report entries!

Example mapping configuration for POSTing violation reports.

  # WASD_CONFIG_MAP
  set * response=csp="img-src the.host.name; \
  report-uri /cgi-bin/cspreport;"

And for the CSP "report-only" option.

  # WASD_CONFIG_MAP
  set * response=cspro="default-src the.host.name; \
  report-uri /cgi-bin/cspreport;"

The utility can be run as a CGI or CGIplus script.

  /cgi-bin/cspreport

CGIplus is more responsive and efficient. 

  /cgiplus-bin/cspreport

When using CGIplus don't forget HTTPD/DO=DCL=DELETE to restart script.

The report file name may contain "YYYY" to have the year substituted, "MM" the
month, "DD" the day, and "HH" the hour substituted as digits.  For example,

  $ DEFINE /SYSTEM CSPREPORT_FILE "LOGS:CSPREPORT_yyyymmdd.LOG"

would record all CSP reports for the day in a single file.  Maximum granularity
is one hour.

Using a browser and accessing the script with no trailing path provides a
report of the current file's content.

  /cgi-bin/cspreport

A wildcard trailing path provides a clickable list of the current file names.

  /cgi-bin/cspreport/*

A file name path provides the content of that file.

  /cgi-bin/cspreport/cspreport_20200216.log


LOGICAL NAMES
-------------
CSPREPORT_DBUG   turns on all "if (dbug)" statements
CSPREPORT_FILE   locates the report storage file
CSPREPORT_MAX    integer defines maximum report size accepted


BUILD DETAILS
-------------
$ @BUILD_CSPREPORT
$ @BUILD_CSPREPORT LINK


COPYRIGHT
---------
Copyright (C) 2020 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY
---------------
15-FEB-2020  MGD  v1.0.0, initial development
*/

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

#define SOFTWAREVN "1.0.0"
#define SOFTWARENM "CSPREPORT"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

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

/* VMS header files */
#include <rmsdef.h>
#include <fab.h>
#include <nam.h>
#include <starlet.h>

/* application related header file */
#include "../misc/cgilib.h"

#define REPORT_SIZE_MAX 4096  /* bytes */

/* global storage */
int  dbug,
     IsCgiPlus,
     ReportSizeMax = REPORT_SIZE_MAX;

unsigned long  Delta100mS [2] = { -1000000, -1 };

unsigned long  BinTime [2];
unsigned short  NumTime [7];

char  *ReportFilePtr;

void OutputReport (char*);
void PostReport (void);
void ProcessRequest (void);
char* ReportFileName (void);
int ReportList (char*);

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

main (int argc, char* argv[])
       
{
   char  *cptr;

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

   if (dbug = (getenv ("CSPREPORT_DBUG") != NULL))
   {
      CgiLibEnvironmentSetDebug (dbug);
      fprintf (stdout, "Content-Type: text/plain\r\n\r\n");
   }

   if (!(ReportFilePtr = getenv ("CSPREPORT_FILE")))
   {
      CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "CSPREPORT_FILE not defined!\n");
      return;
   }

   if (cptr = getenv ("CSPREPORT_MAX"))
   {
      ReportSizeMax = atol(cptr);
      if (ReportSizeMax <= 0) ReportSizeMax = REPORT_SIZE_MAX;
   }

   CgiLibEnvironmentInit (argc, argv, 0);

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();

   if (IsCgiPlus)
   {
      for (;;)
      {
         /* block waiting for the next request */
         CgiLibVar ("");
         ProcessRequest ();
         CgiLibCgiPlusEOF ();
      }
   }
   else
      ProcessRequest ();
}

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

void ProcessRequest (void)
       
{
   char  *cptr;

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

   if (dbug) fprintf (stdout, "ProcessRequest()\n");

   sys$gettim (&BinTime);
   sys$numtim (&NumTime, &BinTime);

   cptr = CgiLibVar ("WWW_REQUEST_METHOD"); 

   if (!strcmp (cptr, "POST"))
      PostReport ();
   else
   if (!strcmp (cptr, "GET"))
   {
      cptr = CgiLibVar ("WWW_PATH_INFO");
      if (strchr (cptr,'*'))
      {
         if (!ReportList (cptr)) OutputReport (NULL);
      }
      else
         OutputReport (cptr);
   }
   else
   {
      CgiLibResponseHeader (403, "text/plain");
      fprintf (stdout, "Hmmm...\n");
      return;
   }
}

/*****************************************************************************/
/*
Append the JSON formatted report POSTed by the browser.
*/

void PostReport (void)
       
{
   int  retry,
        Context,
        PostBufferCount;
   char  *cptr, 
         *VariablePtr,
         *PostBufferPtr;
   FILE  *rfp;

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

   if (dbug) fprintf (stdout, "PostReport()\n");

   cptr = CgiLibVar ("WWW_CONTENT_LENGTH");
   if (atol(cptr) > ReportSizeMax)
   {
      CgiLibResponseHeader (400, "text/plain");
      fprintf (stdout, "Report exceeds maximum size!\n");
      return;
   }


   /* read the POSTed request body */
   CgiLibReadRequestBody (&PostBufferPtr, &PostBufferCount);
   if (PostBufferPtr == NULL || PostBufferCount == 0)
   {
      CgiLibResponseHeader (400, "text/plain");
      fprintf (stdout, "No report in the POST!\n");
      return;
   }

   cptr = CgiLibVar ("WWW_CONTENT_TYPE");

   if (!strcmp (cptr, "application/csp-report"))
   {
      /* try to open the file for 10 seconds */
      for (retry = 100; retry; retry--)
      {
         rfp = fopen (ReportFileName(), "a+", "shr=get,put");
         if (rfp) break;
         if (vaxc$errno != RMS$_FLK) break;
         sys$schdwk (0, 0, &Delta100mS, 0);
         sys$hiber ();
         continue;
      }
      if (!rfp)
      {
         CgiLibResponseHeader (500, "text/plain");
         fprintf (stdout, "Report fopen() %%X%08.08X!\n", vaxc$errno);
      }
      else
      {
         fprintf (rfp,
"%04.04u-%02.02u-%02.02u %02.02u:%02.02u:%02.02u\n%s\n",
                  NumTime[0], NumTime[1], NumTime[2],
                  NumTime[3], NumTime[4], NumTime[5],
                  PostBufferPtr);
         fclose (rfp);
      }
      CgiLibResponseHeader (200, "text/plain", "Content-Length: 0\r\n");
   }
   else
   {
      CgiLibResponseHeader (400, "text/plain");
      fprintf (stdout, "Content not \"application/csp-report\"!\n");
   }

   free (PostBufferPtr);
}

/*****************************************************************************/
/*
If |fname| is NULL or empty return the content of the current report file,
otherwise the content of the specified file name (nam, not fill specification).
*/

void OutputReport (char *fname)
       
{
   int  retry;
   char  *cptr, *sptr, *zptr;
   char  line [1024],
         FileName [256];
   FILE  *rfp;

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

   if (dbug) fprintf (stdout, "OutputReport() |%s|\n", fname);

   if (!CgiLibVar ("WWW_REMOTE_USER")[0])
   {
      CgiLibResponseHeader (403, "text/plain");
      fprintf (stdout, "Must be authenticated!\n");
      return;
   }

   if (fname && fname[0] && fname[0] != '/')
   {
      zptr = (sptr = FileName) + sizeof(FileName)-1;
      for (cptr = ReportFileName(); *cptr && sptr < zptr; *sptr++ = *cptr++);
      if (sptr > FileName) sptr--;;
      while (sptr > FileName && *sptr != ']' && *sptr != ':') sptr--;
      if (sptr > FileName) sptr++;
      if (*fname == '/') fname++;
      for (cptr = fname; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';
      fname = FileName;
   }
   else
      fname = ReportFileName();

   /* try to open the file for 10 seconds */
   for (retry = 100; retry; retry--)
   {
      rfp = fopen (fname, "r", "shr=get,put");
      if (rfp) break;
      if (vaxc$errno != RMS$_FLK) break;
      sys$schdwk (0, 0, &Delta100mS, 0);
      sys$hiber ();
      continue;
   }
   if (!rfp)
   {
      CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "Report fopen() %s %s.\n",
               fname, strerror(errno));
      return;
   }

   CgiLibResponseHeader (200, "text/html");
   fprintf (stdout,
"<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body>\n<pre>\n%s %s\n",
            SOFTWAREID, fname);
   while (fgets (line, sizeof(line), rfp))
   {
      for (cptr = line; *cptr; cptr++);
      if (cptr > line && *(cptr-1) == '\n') *(cptr-1) = '\0';
      if (line[0] == '{')
         fprintf (stdout,
"<script>\n\
var report = JSON.parse(\'%s\');\n\
document.write(JSON.stringify(report,undefined,4));</script>\n",
                  line);
      else   
         fprintf (stdout, "%s ", line);
   }
   fprintf (stdout, "<script>document.close()</script>\n\
</pre>\n</body>\n</html>\n");
   fclose (rfp);
}

/*****************************************************************************/
/*
Generate a clickable list of file names containing CSP reports.
*/

int ReportList (char *wcard)
       
{
   int  count, status;
   char  *aptr, *cptr, *sptr;
   char  size [32],
         FileName [256],
         FileSpec [256],
         ExpFileName [256];
   struct stat  stbuf;
   struct FAB  SearchFab;
   struct NAM  SearchNam;

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

   if (dbug) fprintf (stdout, "ReportList() |%s|\n", wcard);

   strcpy (FileSpec, ReportFilePtr);
   if (!(sptr = strstr (FileSpec, "YYYY")))
      sptr = strstr (FileSpec, "yyyy");
   if (!sptr) return (0);
   for (cptr = sptr; *cptr; cptr++);
   if (cptr > sptr) cptr--;
   while (cptr > sptr && *cptr != '.') cptr--;
   if (cptr <= sptr) return (0);
   /* modify the "YYYY" into a wildcard and move the file type up */
   *sptr++ = '*';
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (dbug) fprintf (stdout, "|%s|\n", FileSpec);
   
   /* initialize the file access block */
   SearchFab = cc$rms_fab;
   SearchFab.fab$l_fna = FileSpec;
   SearchFab.fab$b_fns = strlen(FileSpec);
   SearchFab.fab$l_nam = &SearchNam;

   SearchNam = cc$rms_nam;
   SearchNam.nam$l_esa = ExpFileName;
   SearchNam.nam$b_ess = sizeof(ExpFileName)-1;
   SearchNam.nam$l_rsa = FileName;
   SearchNam.nam$b_rss = sizeof(FileName)-1;

   if ((status = sys$parse (&SearchFab, 0, 0)) & 1 == 0)
   {
      CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "Report $parse() %s %%X%08.08X.\n",
               FileSpec, status);
      return (-1);
   }

   if (!(sptr = CgiLibVar ("WWW_SCRIPT_NAME")))
   {
      CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "Script name?\n");
      return (-1);
   }

   count = 0;
   while ((status = sys$search (&SearchFab, 0, 0)) & 1)
   {
      *SearchNam.nam$l_ver = '\0';
      if (dbug) fprintf (stdout, "FileName |%s|\n", FileName);
      if (stat (FileName, &stbuf))
      {
         status = vaxc$errno;
         break;
      }
      if (!count)
      {
         CgiLibResponseHeader (200, "text/html");
         fprintf (stdout,
"<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body>\n<pre>\n%s %s\n",
                  SOFTWAREID, FileSpec);
      }
      count++;
      if (stbuf.st_size < 1000)
         sprintf (size, "%uB", stbuf.st_size);
      else
      if (stbuf.st_size < 1000000)
         sprintf (size, "%ukB", stbuf.st_size / 1000);
      else
      if (stbuf.st_size < 1000000000)
         sprintf (size, "%uMB", stbuf.st_size / 1000000);
      else
         sprintf (size, "%uGB", stbuf.st_size / 1000000000);
      fprintf (stdout, "<a href=\"%s/%s\">%s</a> %s\n",
               sptr, SearchNam.nam$l_name, SearchNam.nam$l_name, size);
      *SearchNam.nam$l_ver = ';';
   }

   if (dbug) fprintf (stdout, "%%X%08.08X\n", status);
   if (status != RMS$_FNF && status != RMS$_NMF)
   {
      if (!count) CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "Report $parse() %s %%X%08.08X.\n",
               FileSpec, status);
      return (-1);
   }

   if (count) fprintf (stdout, "</pre>\n</body>\n</html>\n");

   return (count);
}

/*****************************************************************************/
/*
Return a pointer to the report (possibly time-stamped) file name.
If the file name contains "YYYY" then add time components at those locations.
*/

char* ReportFileName (void)
       
{
   static int  PrevDay = -1,
               PrevHour = -1;
   static char FileName [256];
   static char  *year, *month, *day, *hour;

   char  buf [32];

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

   if (dbug) fprintf (stdout, "ReportFileName() |%s|%s|\n",
                       ReportFilePtr, FileName);

   /* minimum report file granularity is one hour */
   if (NumTime[3] == PrevHour && NumTime[2] == PrevDay) return (FileName);

   PrevHour = NumTime[3];
   PrevDay = NumTime[2];
   /* (re)build the file name */
   strcpy (FileName, ReportFilePtr);
   year = month = day = hour = NULL;
   if (!(year = strstr (FileName, "YYYY")))
      year = strstr (FileName, "yyyy");
   if (year)
   {
      sprintf (buf, "%4.4u", NumTime[0]);
      year[0] = buf[0];
      year[1] = buf[1];
      year[2] = buf[2];
      year[3] = buf[3];
      if (!(month = strstr (FileName, "MM")))
         month = strstr (FileName, "mm");
      if (month)
      {
         sprintf (buf, "%02.02u", NumTime[1]);
         month[0] = buf[0];
         month[1] = buf[1];
      }
      if (!(day = strstr (FileName, "DD")))
         day = strstr (FileName, "dd");
      if (day)
      {
         sprintf (buf, "%02.02u", NumTime[2]);
         day[0] = buf[0];
         day[1] = buf[1];
      }
      if (!(hour = strstr (FileName, "HH")))
         hour = strstr (FileName, "hh");
      if (hour)
      {
         sprintf (buf, "%02.02u", NumTime[4]);
         hour[0] = buf[0];
         hour[1] = buf[1];
      }
   }

   if (dbug) fprintf (stdout, "|%s|\n", FileName);

   return (FileName);
}

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

