/*****************************************************************************/
/*
                                 ProxyFTP.c

*************
** CAUTION **
*************

THIS MODULE IS TASK-ORIENTED, NOT REQUEST-ORIENTED.

That is, most of the functions take a pointer to proxy task rather than a
pointer to request as do other modules.  Many of these functions deal primarily
and immediately with task rather than request data.

Proxied FTP access (well surprise, surprise! - in Gomer Pyle intonation)

This is a moderately long and complex module providing a basic FTP proxy
service capable of stand-alone use (i.e. standard browser) as well with 
non-browser applications such as "Windows Commander".


CONTENT TYPE
------------
The content-type of a remote file is determined by the proxy FTP server using
the loaded configuration content types.  Why?  Well, why not?  FTP contains so
information as to the file type or how it should be treated.  The proxy server
interpreting it at the local end from the file type (extension) is probably as
good as anything.  To provide some further control on how a file is transfered
the {AddType] and [MimeTypes] directives support syntax to indicated how FTP
should transfer the file.  If the proxy server interpretation is incorrect then
the client can explicitly specify how the file should be handled.


FILE PATHS
----------
By default all file paths are relative to the login directory.  That is path
"/dir2/dir3/file.txt" in anonymous login to "/pub/" refers to the file
"/pub/dir2/dir3/file.txt".  The server would do a "CWD ./pub/dir2/dir3/" before
beginning any other activity.  If the request file path begins with consecutive
forward-slashes then it will be considered an absolute path and for path
"//pub/dir2/dir3/file.txt"  it would do a "CWD /pub/dir2/dir3/" before any
other activity.


QUERY STRING MODIFIERS
----------------------
The following query string keywords may be used to control the behaviour of the
FTP proxy.  One or more may be used in appropriate combinations.  Unknown
strings are ignored without warning.  Many of these are provided to assist in
working around proxy server misinterpretation of remote resources.  The client
may explicitly specify how the resource will be accessed or interpreted.

  dos             process listing as if DOS format
  unix            process listing as if Unix format
  vms             process listing as if VMS format

  text            file(s) returned as "text/plain"
  octet           file(s) returned as "application/octet-stream"
  ascii           retrieve file as ASCII (i.e. text)
  image           retrieve file as a bag-o'-bytes (i.e. binary)
  content:string  content-type of file(s) (e.g. "?content:text/plain")
  content=string  content-type of file(s) (e.g. "?content=text/plain")

  alt             provide alternate access via listing icon
  email:string    for anonymous FTP servers insisting on a valid email-password
  email=string    for anonymous FTP servers insisting on a valid email-password
  list            display in plain-text the LIST command transfer
  login           server to get username/password via HTTP authentication
  raw             do not munge file name/size/date
  upload          supply a form that allows a file to be uploaded

Keywords parsing is designed so that they can be supplied as "?keyword",
"?keyword1+keyword2", "keyword=anything", or
"keyword1=anything&keyword2=anything", or where the keyword occurs as the value
of a form field, for example "?type=ascii".  This allows for simply adding the
keyword to the URL or use in an HTML form.


USING THE "LOGIN" QUERY STRING
------------------------------
The usual mechanism for supplying the username and password for access to a
non-anonymous proxied FTP server area is to place it as part of the request
line (i.e. "ftp://username:password@the.host.name/path/").  This has the
obvious disadvantage that it's there for all and sundry to see, in the browser
URL field, in the server logs, in any other caching agent along the network. 
Of course all plain-text authentication suffers this fate to a greater or
lesser extent.

The "login" query string is provided to work around the more obvious of these
issues, having the authentication credentials as part of the request URL.  When
this string is placed in the request query string the FTP proxy requests the
browser to prompt for authentication (i.e. returns a 401 status).  When request
header authentication data is present it decodes the username and password and
uses this as the remote FTP server username and password.  Hence the remote
username and password never need to appear in plain-text on screen or in server
logs.


NON-BROWSER PROXY BEHAVIOUR
---------------------------
The ability to interact with non-bowser clients has been refined and tested
against "Windows Commander v4.54".  Other clients YMMV.


VERSION HISTORY
---------------
10-JUL-2004  MGD  bugfix; ProxyFtpPasvData() if PASV response address
                  is 0.0.0.0 then use connect address
10-APR-2004  MGD  modifications to support IPv6
12-JAN-2004  MGD  bugfix; ProxyFtpListProcessUnix() maximum fields handling
                  (thanks Jean-Pierre Petit, jpp@esme.fr)
08-MAR-2003  MGD  set html= FTP directory listing header and footer,
                  add column headings to FTP Index of,
                  bugfix; ResponseHeader() content-length of zero
29-MAY-2002  MGD  allow for a server disconnecting immediately upon QUIT
23-MAR-2002  MGD  FTP agent specifics, starting with "MadGoat" DELE (arghh!)
18-MAR-2002  MGD  rework directory list processing
26-JAN-2002  MGD  initial
*/
/*****************************************************************************/

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

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

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

#define WASD_MODULE "PROXYFTP"

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

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

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

extern int  EfnWait,
            EfnNoWait,
            HttpdTickSecond,
            NetReadBufferSize,
            ProxyReadBufferSize;

extern char  ConfigContentTypeBlank[],
             ConfigContentTypeDir[],
             ConfigDefaultFileContentType[],
             ErrorSanityCheck[],
             ServerHostPort[],
             SoftwareID[],
             Utility[];

extern struct dsc$descriptor TcpIpDeviceDsc;

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern IPADDRESS  TcpIpEmptyAddress;
extern MSG_STRUCT  Msgs;
extern PROXY_ACCOUNTING_STRUCT  *ProxyAccountingPtr;
extern SERVICE_STRUCT  *ServiceListHead;
extern TCP_SOCKET_ITEM  TcpIpSocket4,
                        TcpIpSocket6;
extern VMS_ITEM_LIST2  TcpIpFullDuplexCloseOption;
extern WATCH_STRUCT  Watch;

/****************************************************************************/
/*
Controls the state and state transitions of a proxied FTP request.
Generally it's from top to bottom but of course different paths may be taken.
*/

ProxyFtpLifeCycle (PROXY_TASK *tkptr)

{
   char  *cptr, *sptr, *zptr;
   char  String [256];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpLifeCycle() !&F !UL !UL",
                 &ProxyFtpLifeCycle, tkptr->FtpState, tkptr->FtpResponseCode);
 
   rqptr = tkptr->RequestPtr;

   switch (tkptr->FtpState)
   {
      /*********/
      /* login */
      /*********/

      case PROXY_FTP_STATE_NONE :  /* actually zero! */

         InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpCount);

         if (!ProxyFtpFilePath (tkptr)) goto QuitNow;

         /* determine if a directory listing or file transfer */
         cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
         while (cptr > tkptr->FtpFilePathPtr &&
                *cptr != '/' && *cptr != '*') cptr--;
         if (*cptr == '*')
         {
            tkptr->FtpDirList = true;
            while (cptr > tkptr->FtpFilePathPtr && *cptr != '/') cptr--;
            if (*cptr == '/') cptr++;
            tkptr->FtpWildPtr = cptr;
         }
         else
         if (*(unsigned short*)cptr == '/\0')
         {
            tkptr->FtpDirList = true;
            tkptr->FtpWildPtr = "";
         }
         else
            tkptr->FtpDirList = false;

         /* read the FTP server's announcement */
         tkptr->FtpState = PROXY_FTP_STATE_USER;
         ProxyFtpResponse (tkptr);
         break;

      case PROXY_FTP_STATE_USER :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         tkptr->FtpState = PROXY_FTP_STATE_USER_DONE;
         if (rqptr->RemoteUser[0])
            ProxyFtpCommand (tkptr, true, "USER !AZ", rqptr->RemoteUser);
         else
         if (tkptr->UrlUserName[0])
            ProxyFtpCommand (tkptr, true, "USER !AZ", tkptr->UrlUserName);
         else
            ProxyFtpCommand (tkptr, true, "USER anonymous");
         break;

      case PROXY_FTP_STATE_USER_DONE :

         if (tkptr->FtpResponseClass > 3)
         {
            InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpLoginFailCount);
            goto QuitNow;
         }
         if (tkptr->FtpResponseCode == 331)
         {
            tkptr->FtpState = PROXY_FTP_STATE_PASS_DONE;
            if (rqptr->RemoteUser[0])
               ProxyFtpCommand (tkptr, true, "PASS !AZ",
                                rqptr->RemoteUserPassword);
            else
            if (tkptr->UrlPassword[0])
               ProxyFtpCommand (tkptr, true, "PASS !AZ",
                                tkptr->UrlPassword);
            else
            if (tkptr->UrlUserName[0])
               ProxyFtpCommand (tkptr, true, "PASS !AZ@!AZ",
                                tkptr->UrlUserName,
                                rqptr->ServicePtr->ServerIpAddressString);
            else
            {
               cptr = rqptr->rqHeader.QueryStringPtr;
               if (cptr[0] && (sptr = ProxyFtpInQueryString (cptr, "email:")))
               {
                  /* keyword style */
                  cptr = sptr + 6;
                  zptr = (sptr = String) + sizeof(String)-1;
                  while (*cptr && *cptr != '+' && sptr < zptr)
                     *sptr++ = *cptr++;
                  *sptr = '\0';
                  ProxyFtpCommand (tkptr, true, "PASS !AZ", String);
               }
               else
               if (cptr[0] && (sptr = ProxyFtpInQueryString (cptr, "email=")))
               {
                  /* form field style */
                  cptr = sptr + 6;
                  zptr = (sptr = String) + sizeof(String)-1;
                  while (*cptr && *cptr != '&' && sptr < zptr)
                     *sptr++ = *cptr++;
                  *sptr = '\0';
                  ProxyFtpCommand (tkptr, true, "PASS !AZ", String);
               }
               else
                  ProxyFtpCommand (tkptr, true, "PASS proxy@!AZ",
                                   rqptr->ServicePtr->ServerHostName);
            }
            break;
         }
         /* otherwise just drop thru, password not required! */

      case PROXY_FTP_STATE_PASS_DONE :

         if (tkptr->FtpResponseClass > 3)
         {
            InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpLoginFailCount);
            goto QuitNow;
         }
         if (tkptr->FtpResponseCode == 230) ProxyFtpResponse230 (tkptr);
         tkptr->FtpState = PROXY_FTP_STATE_SYST_DONE;
         ProxyFtpCommand (tkptr, true, "SYST");
         break;

      /*****************/
      /* now logged in */
      /*****************/

      case PROXY_FTP_STATE_SYST_DONE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         zptr = (sptr = tkptr->FtpSYST) + sizeof(tkptr->FtpSYST)-1;
         for (cptr = tkptr->FtpResponsePtr+4; *cptr && ISLWS(*cptr); cptr++);
         while (*cptr && NOTEOL(*cptr) && sptr < zptr)
         {
            if (*cptr == '\"')
               *sptr++ = '\'';
            else
               *sptr++ = *cptr;
            cptr++;
         }
         *sptr = '\0';
         if (strstr (tkptr->FtpSYST, "MadGoat"))
            tkptr->FtpSpecific = PROXY_FTP_SPECIFIC_MADGOAT;
         tkptr->FtpState = PROXY_FTP_STATE_PWD_DONE;
         ProxyFtpCommand (tkptr, true, "PWD");
         break;

      case PROXY_FTP_STATE_PWD_DONE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         ProxyFtpRemoteFileSystem (tkptr);
         if (tkptr->HttpMethod == HTTP_METHOD_DELETE)
            tkptr->FtpState = PROXY_FTP_STATE_DELE;
         else
            tkptr->FtpState = PROXY_FTP_STATE_CWD_DONE;
         ProxyFtpCwd (tkptr);
         break;

      case PROXY_FTP_STATE_CWD_DONE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         zptr = (sptr = tkptr->FtpCWD) + sizeof(tkptr->FtpCWD)-1;
         for (cptr = tkptr->FtpResponsePtr+4; *cptr && ISLWS(*cptr); cptr++);
         while (*cptr && NOTEOL(*cptr) && sptr < zptr)
         {
            if (*cptr == '\"')
               *sptr++ = '\'';
            else
               *sptr++ = *cptr;
            cptr++;
         }
         *sptr = '\0';
         tkptr->FtpState = PROXY_FTP_STATE_PASV_DONE;
         ProxyFtpCommand (tkptr, true, "PASV");
         break;

      case PROXY_FTP_STATE_PASV_DONE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;

         /* process the IP address and port supplied by the PASV response */
         if (!ProxyFtpPasvData (tkptr))
         {
            ProxyFtpResponseInvalid (tkptr);
            goto QuitNow;
         }

         if (tkptr->HttpMethod == HTTP_METHOD_POST ||
             tkptr->HttpMethod == HTTP_METHOD_PUT)
            tkptr->FtpState = PROXY_FTP_STATE_STOR;
         else
         if (tkptr->HttpMethod == HTTP_METHOD_GET)
         {
            /* determine if a directory listing or file transfer */
            cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
            while (cptr > tkptr->FtpFilePathPtr &&
                   *cptr != '/' && *cptr != '*') cptr--;
            if (*cptr == '*' || *(unsigned short*)cptr == '/\0')
               tkptr->FtpState = PROXY_FTP_STATE_LIST;
            else
               tkptr->FtpState = PROXY_FTP_STATE_RETR_MODE;
         }
         else
            ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

         /* connect to the address and port specified by the PASV response */
         ProxyFtpDataConnect (tkptr);
         break;

      /*********************/
      /* directory listing */
      /*********************/

      case PROXY_FTP_STATE_LIST :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* send the file system specific command for a directory listing */
         tkptr->FtpState = PROXY_FTP_STATE_LIST_RECEIVE;
         ProxyFtpList (tkptr);
         break;

      case PROXY_FTP_STATE_LIST_RECEIVE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         tkptr->FtpState = PROXY_FTP_STATE_LIST_CHECK;
         /* transfer the listing */
         ProxyFtpListReceive (tkptr);
         break;

      case PROXY_FTP_STATE_LIST_CHECK :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* get so we can check the transfer complete response */
         tkptr->FtpState = PROXY_FTP_STATE_LIST_PROCESS;
         ProxyFtpResponse (tkptr);
         break;

      case PROXY_FTP_STATE_LIST_PROCESS :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* format and output the directory listing */
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         ProxyFtpListProcess (tkptr);
         break;

      /*********************/
      /* retrieving a file */
      /*********************/

      case PROXY_FTP_STATE_RETR_MODE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* send the command for binary or ASCII transfer */
         tkptr->FtpState = PROXY_FTP_STATE_RETR;
         ProxyFtpRetrieveMode (tkptr);
         break;

      case PROXY_FTP_STATE_RETR :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* send the command to retrieve the file */
         tkptr->FtpState = PROXY_FTP_STATE_RETR_FILE;
         ProxyFtpRetrieve (tkptr);
         break;

      case PROXY_FTP_STATE_RETR_FILE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         tkptr->FtpState = PROXY_FTP_STATE_RETR_DONE;
         /* transfer the file */
         ProxyFtpRetrieveAst (tkptr);
         break;

      case PROXY_FTP_STATE_RETR_DONE :

         /* get the response to the file transfer */
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         ProxyFtpResponse (tkptr);
         break;

      /******************/
      /* storing a file */
      /******************/

      case PROXY_FTP_STATE_STOR :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* begin request body (file), will initiate the STOR command */
         tkptr->FtpState = PROXY_FTP_STATE_STOR_TYPE;
         ProxyFtpStoreBodyReadBegin (tkptr);
         break;

      case PROXY_FTP_STATE_STOR_TYPE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* TYPE sent, go back to transfering the request body (file) */
         tkptr->FtpState = PROXY_FTP_STATE_STOR_FILE;
         ProxyFtpStoreBodyReadAst (tkptr->RequestPtr);
         break;

      case PROXY_FTP_STATE_STOR_FILE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* STOR sent, go back to transfering the request body (file) */
         tkptr->FtpState = PROXY_FTP_STATE_STOR_CHECK;
         ProxyFtpStoreBodyReadAst (tkptr->RequestPtr);
         break;

      case PROXY_FTP_STATE_STOR_CHECK :

         /* get the response to the file transfer */
         tkptr->FtpState = PROXY_FTP_STATE_STOR_DONE;
         ProxyFtpResponse (tkptr);
         break;

      case PROXY_FTP_STATE_STOR_DONE :

         /* get the response to the file transfer */
         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         for (cptr = tkptr->FtpResponsePtr; NOTEOL(*cptr); cptr++);
         *cptr = '\0';
         for (cptr = tkptr->FtpResponsePtr+4; ISLWS(*cptr); cptr++);
         ReportSuccess (rqptr, "!AZ &nbsp;!&;AZ",
                        MsgFor(rqptr,MSG_PROXY_FTP_SERVER_REPLIED), cptr);
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         SysDclAst (&ProxyFtpLifeCycle, tkptr);
         break;

      /*******************/
      /* deleting a file */
      /*******************/

      case PROXY_FTP_STATE_DELE :

         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         /* just delete the file */
         tkptr->FtpState = PROXY_FTP_STATE_DELE_DONE;
         ProxyFtpDelete (tkptr);
         break;

      case PROXY_FTP_STATE_DELE_DONE :

         /* get the response to the file transfer */
         if (tkptr->FtpResponseClass > 3) goto QuitNow;
         for (cptr = tkptr->FtpResponsePtr; NOTEOL(*cptr); cptr++);
         *cptr = '\0';
         for (cptr = tkptr->FtpResponsePtr+4; ISLWS(*cptr); cptr++);
         ReportSuccess (rqptr, "!AZ &nbsp;!&;AZ",
                        MsgFor(rqptr,MSG_PROXY_FTP_SERVER_REPLIED), cptr);
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         SysDclAst (&ProxyFtpLifeCycle, tkptr);
         break;

      /********/
      /* quit */
      /********/

      case PROXY_FTP_STATE_QUIT :

         tkptr->FtpState = PROXY_FTP_STATE_QUIT_DONE;
         ProxyFtpCommand (tkptr, true, "QUIT");
         break;

      case PROXY_FTP_STATE_ABORT :
      case PROXY_FTP_STATE_QUIT_DONE :

         ProxyFtpDataCloseSocket (tkptr);
         ProxyEnd (tkptr);
         break;

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

   return;

   /* where's Dijkstra when you really need him? */
   QuitNow :

      for (cptr = tkptr->FtpResponsePtr; NOTEOL(*cptr); cptr++);
      *cptr = '\0';
      for (cptr = tkptr->FtpResponsePtr+4; ISLWS(*cptr); cptr++);
      /* bogus status code 599 is used to represent WASD generated errors */
      if (tkptr->FtpResponseCode < 599)
      {
         ProxyFtpHttpStatus (tkptr);
         WriteFao (String, sizeof(String), NULL, "!AZ &nbsp;!&;AZ",
                   MsgFor(rqptr,MSG_PROXY_FTP_SERVER_REPLIED), cptr);
         ErrorGeneral (rqptr, String, FI_LI);
      }
      else
      {
         rqptr->rqResponse.HttpStatus = 502;
         ErrorGeneral (rqptr, tkptr->FtpResponsePtr+4, FI_LI);
      }
      tkptr->FtpState = PROXY_FTP_STATE_QUIT;
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Format a command string and write it to the FTP server.  Optionally get a
response back from the server.  If 'GetResponse' is true this function will AST
to ProxyFtpResponse() to initiate the read.  In this case the AST function
should check the read IO status block for success.  If no response is required
the AST function should check the write IO status block.  If 'FormatString' is
NULL then consider the command buffer to already contain a formatted command.
*/

ProxyFtpCommand
(
PROXY_TASK *tkptr,
BOOL GetResponse,
char *FormatString,
...
)
{
   int  argcnt, status;
   unsigned long  *vecptr;
   unsigned long  FaoVector [32];
   PROXY_AST  AstFunction;
   REQUEST_STRUCT  *rqptr;
   va_list  argptr;

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

   va_count (argcnt);

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpCommand() !&B !UL !&Z",
                 GetResponse, argcnt, FormatString);
 
   rqptr = tkptr->RequestPtr;

   if (!tkptr->FtpCommandPtr)
   {
      /* first command issued, allocate buffer space */
      tkptr->FtpCommandSize = PROXY_FTP_COMMAND_SIZE;
      tkptr->FtpCommandPtr = VmGetHeap (rqptr, tkptr->FtpCommandSize);
   }

   if (FormatString)
   {
      if (argcnt > 32+3)
         ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

      vecptr = FaoVector;
      va_start (argptr, FormatString);
      for (argcnt -= 3; argcnt; argcnt--)
         *vecptr++ = va_arg (argptr, unsigned long);
      va_end (argptr);

      status = WriteFormatted (NULL,
                               tkptr->FtpCommandPtr,
                               tkptr->FtpCommandSize-2,
                               &tkptr->FtpCommandCount,
                               FormatString, &FaoVector);
      if (VMSnok (status))
      {
         tkptr->ProxyWriteIOsb.Status = status;
         tkptr->ProxyWriteIOsb.Count = 0;
         SysDclAst (&ProxyFtpLifeCycle, tkptr);
         return;
      }
      /* append required carriage-control */
      tkptr->FtpCommandPtr[tkptr->FtpCommandCount++] = '\r';
      tkptr->FtpCommandPtr[tkptr->FtpCommandCount++] = '\n';
   }

   if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
   {
      char  *cptr, *sptr;
      cptr = sptr = tkptr->FtpCommandPtr;
      while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
      if (*(unsigned long*)sptr == 'PASS')
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                    ">>PASS !#**", cptr-sptr-5);
      else
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                    ">>!#AZ", cptr-sptr, sptr);
   }

   if (GetResponse)
      AstFunction = &ProxyFtpCommandResponseAst;
   else
      AstFunction = &ProxyFtpCommandAst;

   ProxyWriteRaw (tkptr, AstFunction,
                  tkptr->FtpCommandPtr,
                  tkptr->FtpCommandCount);
}

/****************************************************************************/
/*
Check the write status and if successful AST back to the function originally
supplied to ProxyFtpCommand().  If command write unsuccessful terminate the
request.
*/

ProxyFtpCommandAst (PROXY_TASK *tkptr)

{
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpCommandAst() !&F !&S !UL", &ProxyFtpCommandAst,
                 tkptr->ProxyWriteIOsb.Status, tkptr->ProxyWriteIOsb.Count);

   if (VMSok (tkptr->ProxyWriteIOsb.Status))
   {
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   /* error writing to the remote FTP server */
   rqptr = tkptr->RequestPtr;
   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, tkptr->ProxyWriteIOsb.Status, FI_LI);

   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
This is AST is used only if 'GetResponse' was true.  Check the write status and
if successful read the FTP server's response.  If command write unsuccessful
terminate the request.
*/

ProxyFtpCommandResponseAst (PROXY_TASK *tkptr)

{
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpCommandResponseAst() !&F !&S !UL",
                 &ProxyFtpCommandAst,
                 tkptr->ProxyWriteIOsb.Status, tkptr->ProxyWriteIOsb.Count);

   if (VMSok (tkptr->ProxyWriteIOsb.Status))
   {
      ProxyFtpResponse (tkptr);
      return;
   }

   /* error writing to the remote FTP server */
   rqptr = tkptr->RequestPtr;
   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, tkptr->ProxyWriteIOsb.Status, FI_LI);

   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Read the response from the remote FTP server.
*/

ProxyFtpResponse (PROXY_TASK *tkptr)

{
   char  *cptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpResponse() !&F !&S !UL !&X", &ProxyFtpResponse,
                 tkptr->ProxyWriteIOsb.Status, tkptr->ProxyWriteIOsb.Count,
                 tkptr->FtpNextResponsePtr);

   rqptr = tkptr->RequestPtr;

   if (!tkptr->FtpResponsePtr)
   {
      /* first response, allocate receive buffer */
      tkptr->FtpResponseSize = ProxyReadBufferSize;
      tkptr->FtpResponsePtr = VmGetHeap (rqptr, tkptr->FtpResponseSize);
      tkptr->FtpResponseCurrentPtr = tkptr->FtpResponsePtr;
   }

   if (tkptr->FtpResponseCurrentPtr == tkptr->FtpResponsePtr)
   {
      /* start of new response, reset buffer and data */
      tkptr->FtpResponseRemaining = tkptr->FtpResponseSize;
      tkptr->FtpResponseCode = tkptr->FtpResponseClass =
         tkptr->FtpResponseCount = tkptr->FtpResponseLineCount = 0;
   }

   if (tkptr->FtpNextResponsePtr)
   {
      /* previous "response" contained at least two responses! */
      for (cptr = tkptr->FtpNextResponsePtr; *cptr; cptr++);
      memcpy (tkptr->FtpResponsePtr,
              tkptr->FtpNextResponsePtr,
              cptr - tkptr->FtpNextResponsePtr+1);
      /* fudge this an an actual network read! */
      tkptr->ProxyReadIOsb.Status = SS$_NORMAL;
      tkptr->ProxyReadIOsb.Count = cptr - tkptr->FtpNextResponsePtr;
      SysDclAst (&ProxyFtpResponseAst, tkptr);
      return;
   }

   if (tkptr->FtpResponseRemaining < PROXY_FTP_RESPONSE_LOW_BUFFER)
   {
      /* buffer space is getting a bit low, reallocate */
      tkptr->FtpResponseSize += ProxyReadBufferSize; 
      tkptr->FtpResponseRemaining += ProxyReadBufferSize;
      tkptr->FtpResponsePtr =
          VmReallocHeap (rqptr, tkptr->FtpResponsePtr,
                         tkptr->FtpResponseSize, FI_LI);
      tkptr->FtpResponseCurrentPtr = tkptr->FtpResponsePtr +
                                     tkptr->FtpResponseSize -
                                     tkptr->FtpResponseRemaining;
   }

   ProxyReadRaw (tkptr, &ProxyFtpResponseAst,
                 tkptr->FtpResponseCurrentPtr,
                 tkptr->FtpResponseRemaining);
}

/****************************************************************************/
/*
A response has been read from the FTP server.  If successful AST to the
function originally supplied to ProxyFtpRequest(), if unsuccessful just
terminate the request.
*/

ProxyFtpResponseAst (PROXY_TASK *tkptr)

{
   BOOL ResponseContinued;
   int  ResponseCount;
   char  *cptr, *dptr, *sptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpResponseAst() !&F !&S !UL",
                 &ProxyFtpResponseAst,
                 tkptr->ProxyReadIOsb.Status, tkptr->ProxyReadIOsb.Count);

   ResponseCount = 0;
   tkptr->FtpNextResponsePtr = NULL;

   if (VMSok (tkptr->ProxyReadIOsb.Status))
   {
      cptr = tkptr->FtpResponseCurrentPtr;

      tkptr->FtpResponseCount += tkptr->ProxyReadIOsb.Count;
      tkptr->FtpResponseCurrentPtr += tkptr->ProxyReadIOsb.Count;
      tkptr->FtpResponseRemaining -= tkptr->ProxyReadIOsb.Count;
      *tkptr->FtpResponseCurrentPtr = '\0';

      for (sptr = cptr; *sptr; sptr++)
      {
         if (*sptr != '\r' && *sptr != '\n') continue;
         /* some Microsoft servers seem to "\r\r\n" (?) e.g. Compaq */
         if (*(unsigned long*)sptr & 0xffffff00 == '\r\r\n\0')
         {
            /* just turn the leading '\r' into a (hopefully) harmless space */
            *sptr = ' ';
            continue;
         }
         while (*sptr == '\r' || *sptr == '\n') sptr++;
         if (!*sptr)
         {
            sptr = NULL;
            break;
         }
      }
      if (sptr)
      {
         /* response not yet terminated by carriage control, get more */
         ProxyFtpResponse (tkptr);
         return;
      }

      /* non-continued response terminated by carriage-control */
      ResponseContinued = false;
      cptr = tkptr->FtpResponsePtr;
      for (;;)
      {
         if (cptr > tkptr->FtpResponsePtr && *cptr == ' ')
         {
            /* a 'logical' line with embedded carriage-control */
            cptr++;
         }
         else
         if ((cptr[0] == '1' ||
              cptr[0] == '2' ||
              cptr[0] == '3' ||
              cptr[0] == '4' ||
              cptr[0] == '5') &&
              isdigit(cptr[1]) &&
              isdigit(cptr[2]))
         {
            /* establish the success or otherwise of the response */
            if (!tkptr->FtpResponseCode)
            {
               /* use the status of the first response line */
               tkptr->FtpResponseClass = cptr[0] - '0';
               tkptr->FtpResponseCode = atoi(cptr);
            }
            if (cptr[3] == '-')
               ResponseContinued = true;
            else
            {
               ResponseContinued = false;
               if (ResponseCount++) tkptr->FtpNextResponsePtr = cptr;
            }
         }
         else
         {
           if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
              WatchDataDump (tkptr->FtpResponsePtr,
                             tkptr->FtpResponseCount);
            ProxyFtpResponseInvalid (tkptr);
            return;
         }

         /* find the start of the next 'physical' line */
         while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
         while (*cptr == '\r' || *cptr == '\n') cptr++;
         tkptr->FtpResponseLineCount++;
         if (!*cptr) break;
      }

      /* end of buffer */
      if (ResponseContinued)
      {
         /* this response is to be continued with at least another line */
         ProxyFtpResponse (tkptr);
      }
      else
      {
         /* indicate that the next response read is a totally fresh one */
         tkptr->FtpResponseCurrentPtr = tkptr->FtpResponsePtr;

         if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
         {
            sptr = tkptr->FtpResponsePtr;
            cptr--;
            while (cptr > sptr && *cptr == '\r' || *cptr == '\n') cptr--;
            if (cptr > sptr) cptr++;
            WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                       "<<!#AZ", cptr-sptr, sptr);
         }

         SysDclAst (&ProxyFtpLifeCycle, tkptr);
      }
      return;
   }

   if (tkptr->FtpState == PROXY_FTP_STATE_QUIT_DONE &&
       tkptr->FtpDataReadIOsb.Status == SS$_LINKDISCON)
   {
      /* can live with the FTP server disconnecting immediately upon QUIT */
      tkptr->FtpDataReadIOsb.Status = SS$_NORMAL;
      tkptr->FtpResponseClass = 2;
      tkptr->FtpResponseCode = 221;
      tkptr->FtpResponsePtr = "221 Session was disconnected by remote server!";
      tkptr->FtpResponseCurrentPtr = tkptr->FtpResponsePtr;
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   /* error reading from the remote FTP server */
   rqptr = tkptr->RequestPtr;
   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, tkptr->ProxyReadIOsb.Status, FI_LI);

   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Couldn't understand the FTP server's response.  Create a bogus FTP-like status
line and appropriate status values to indicate this.  Set the state to QUIT and
declare the state processor so that the calling routine should return
imediately after calling this function.
*/

ProxyFtpResponseInvalid (PROXY_TASK *tkptr)

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpResponseInvalid()");

   tkptr->FtpResponseClass = 5;
   tkptr->FtpResponseCode = 599;
   strcpy (tkptr->FtpResponsePtr, "599 Invalid response from FTP server.");
   tkptr->FtpResponseCount = strlen(tkptr->FtpResponsePtr);

   tkptr->FtpState = PROXY_FTP_STATE_QUIT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
The server has returned a 230 response status.  This is where site
announcements are often delivered.  Get the contents of this response for use
in the directory listing page.
*/

ProxyFtpResponse230 (PROXY_TASK *tkptr)

{
   char  *cptr, *sptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpResponse230() !UL", tkptr->FtpResponseLineCount);

   if (tkptr->FtpResponseLineCount <= 2) return;

   rqptr = tkptr->RequestPtr;

   /* won't be using -more- than this memory */
   sptr = tkptr->Ftp230Ptr = VmGetHeap (rqptr, tkptr->FtpResponseCount); 
   cptr = tkptr->FtpResponsePtr;
   while (*cptr)
   {
      if (cptr[3] == '-')
      {
         cptr += 4;
         while (*cptr)
         {
            while (*cptr && *cptr != '\r' && *cptr != '\n') *sptr++ = *cptr++;
            while (*cptr == '\r') cptr++;
            if (*cptr == '\n') *sptr++ = *cptr++;
            if (*cptr != ' ') break;
         }
      }
      else
      {
         while (*cptr)
         {
            while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
            while (*cptr == '\n' || *cptr == '\r') cptr++;
            if (*cptr != ' ') break;
         }
      }
   }
   /* looks better on the page if trailing newlines are suppressed */
   while (sptr > tkptr->Ftp230Ptr && *(sptr-1) == '\n') sptr--;
   *sptr = '\0';
   tkptr->Ftp230Length = sptr - tkptr->Ftp230Ptr;
}

/****************************************************************************/
/*
Generate a URL-decoded file path from the request URI.
*/

BOOL ProxyFtpFilePath (PROXY_TASK *tkptr)

{
   int  Length;
   char  *cptr, *sptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpFilePath() !&Z", tkptr->RequestUriPtr);

   rqptr = tkptr->RequestPtr;

   for (sptr = tkptr->RequestUriPtr; *sptr && *sptr != '?'; sptr++);
   Length = sptr - tkptr->RequestUriPtr;
   tkptr->FtpFilePathPtr = VmGetHeap (rqptr, Length+1);
   memcpy (tkptr->FtpFilePathPtr, tkptr->RequestUriPtr, Length);
   tkptr->FtpFilePathPtr[Length] = '\0';
   /* we know the decode will be OK because the path has already been done! */
   Length = StringUrlDecode (tkptr->FtpFilePathPtr);
   if (Length == -1) return (false);
   tkptr->FtpFilePathLength = Length;
   return (true);
}

/****************************************************************************/
/*
Try and determine the remote file system type from the PWD response.
*/

ProxyFtpRemoteFileSystem (PROXY_TASK *tkptr)

{
   char  *cptr, *sptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRemoteFileSystem()");

   tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_UNKNOWN;
   tkptr->FtpFileSystemPtr = "unknown";

   cptr = tkptr->FtpResponsePtr + 4;
   while (*cptr)
   {
      if (*cptr == '/')
      {
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_UNIX;
         tkptr->FtpFileSystemPtr = "Unix";
         InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpUnixCount);
         break;
      }
      if (*cptr == '\\')
      {
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_DOS;
         tkptr->FtpFileSystemPtr = "DOS";
         InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpDosCount);
         break;
      }
      if (*(unsigned short*)cptr == ':[')
      {
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_VMS;
         tkptr->FtpFileSystemPtr = "VMS";
         InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpVmsCount);
         break;
      }
      cptr++;
   }
   if (!*cptr) InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpUnknownCount);

   if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                 "FILE-SYSTEM !AZ", tkptr->FtpFileSystemPtr);
}

/****************************************************************************/
/*
Issue a Change Working Directory (CWD) command.  The URI path specification
needs to be massaged according to file system specifics.
*/

ProxyFtpCwd (PROXY_TASK *tkptr)

{
   int  DirCount;
   char  *cptr, *dptr, *sptr, *zptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY, "ProxyFtpCwd()");

   rqptr = tkptr->RequestPtr;

   DirCount = 0;
   for (cptr = tkptr->FtpFilePathPtr + 1; *cptr; *cptr++)
      if (*cptr == '/') DirCount++;

   if (!DirCount)
   {
      /* root of FTP directory structure, no CWD required */
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   zptr = (sptr = tkptr->FtpCommandPtr) + tkptr->FtpCommandSize - 1;
   *(unsigned long*)sptr = 'CWD ';
   sptr += 4;

   cptr = tkptr->FtpFilePathPtr;
   if (tkptr->FtpFileSystem == PROXY_FTP_FILE_SYSTEM_VMS)
   {
      if (*(unsigned short*)cptr == '//')
      {
         /* absolute path */
         cptr += 2;
         while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
         if (*cptr) cptr++;
         if (sptr < zptr) *sptr++ = ':';
         if (sptr < zptr) *sptr++ = '[';
         DirCount = 0;
         dptr = sptr;
         while (*cptr && sptr < zptr)
         {
            while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
            if (*cptr)
            {
               /* just another directory */
               DirCount++;
               cptr++;
               dptr = sptr;
               if (sptr < zptr) *sptr++ = '.';
            }
         }
         sptr = dptr;
         if (!DirCount)
         {
            if (sptr < zptr) *sptr++ = '0';
            if (sptr < zptr) *sptr++ = '0';
            if (sptr < zptr) *sptr++ = '0';
            if (sptr < zptr) *sptr++ = '0';
            if (sptr < zptr) *sptr++ = '0';
            if (sptr < zptr) *sptr++ = '0';
         }
         *sptr++ = ']';
      }
      else
      {
         if (*cptr) cptr++;
         *sptr++ = '[';
         *sptr++ = '.';
         dptr = sptr;
         while (*cptr && sptr < zptr)
         {
            while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
            if (*cptr)
            {
               /* just another directory */
               cptr++;
               dptr = sptr;
               if (sptr < zptr) *sptr++ = '.';
            }
         }
         sptr = dptr;
         *sptr++ = ']';
      }
   }
   else
   {
      if (*(unsigned short*)cptr != '//')
      {
         /* if not an absolute path */
         *sptr++ = '.';
         *sptr++ = '/';
      }
      if (*cptr) cptr++;
      dptr = sptr;
      while (*cptr && sptr < zptr)
      {
         if (*cptr == '/') dptr = sptr;
         *sptr++ = *cptr++;
      }
      sptr = dptr;
      if (tkptr->FtpFileSystem == PROXY_FTP_FILE_SYSTEM_DOS)
      {
         /* for DOS, turn each forward slash into a back-slash */
         *sptr = '\0';
         for (cptr = tkptr->FtpCommandPtr + 4; *cptr; cptr++)
            if (*cptr == '/') *cptr = '\\';
      }
   }

   if (sptr < zptr) *sptr++ = '\r';
   if (sptr < zptr) *sptr++ = '\n';
   *sptr = '\0';
   if (sptr >= zptr) ErrorNoticed (SS$_BUFFEROVF, "FTP", FI_LI);

   /* length must be set if we're going to play with the buffer directly */
   tkptr->FtpCommandCount = sptr - tkptr->FtpCommandPtr;

   ProxyFtpCommand (tkptr, true, NULL);
}

/****************************************************************************/
/*
Parse the IP address and port from the PASV response into the FTP data IP
address and port storage.  If there's a problem return false otherwise true.
*/

BOOL ProxyFtpPasvData (PROXY_TASK *tkptr)

{
   int  cnt;
   int  PasvOctets [6];
   char  *cptr, *sptr;
   unsigned char  *optr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpPasvData() !&Z", tkptr->FtpResponsePtr);

   cptr = tkptr->FtpResponsePtr + 4;
   while (*cptr != '(') cptr++;
   cnt = sscanf (cptr, "(%d,%d,%d,%d,%d,%d)",
                 &PasvOctets[0], &PasvOctets[1], &PasvOctets[2],
                 &PasvOctets[3], &PasvOctets[4], &PasvOctets[5]); 
   if (cnt != 6) return (false);

   if (IPADDRESS_IS_V4 (&tkptr->ConnectIpAddress))
   {
      IPADDRESS_ZERO4 (&tkptr->FtpDataIpAddress)
      optr = &IPADDRESS_ADR4 (&tkptr->FtpDataIpAddress);
      for (cnt = 0; cnt < 4; cnt++)
      {
         if (PasvOctets[cnt] < 0 || PasvOctets[cnt] > 255) return (false);
         *optr++ = PasvOctets[cnt];
      }
      /* if PASV response address is 0.0.0.0 then use connect address */
      if (!IPADDRESS_IS_SET (&tkptr->FtpDataIpAddress))
         IPADDRESS_COPY (&tkptr->FtpDataIpAddress, &tkptr->ConnectIpAddress)
   }
   else
   if (IPADDRESS_IS_V6 (&tkptr->ConnectIpAddress))
   {
      /* the convention is for IPv6 clients to ignore the supplied address */
      IPADDRESS_COPY (&tkptr->FtpDataIpAddress, &tkptr->ConnectIpAddress)
   }
   else
      return (false);

   tkptr->FtpDataIpPort = 0;
   optr = &tkptr->FtpDataIpPort;
   for (cnt = 4; cnt < 6; cnt++)
   {
      if (PasvOctets[cnt] < 0 || PasvOctets[cnt] > 255) return (false);
      *optr++ = PasvOctets[cnt];
   }

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "!&I,!UL", &tkptr->FtpDataIpAddress, tkptr->FtpDataIpPort);

   if (IPADDRESS_IS_SET (&tkptr->FtpDataIpAddress) && tkptr->FtpDataIpPort)
      return (true);
   else
      return (false);
}

/****************************************************************************/
/*
Generate a directory listing command appropriate to the environment.
*/

ProxyFtpList (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY, "ProxyFtpList()");

   InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpListCount);

   switch (tkptr->FtpFileSystem)
   {
      case PROXY_FTP_FILE_SYSTEM_DOS :
         ProxyFtpCommand (tkptr, true, "NLIST!AZ!AZ",
                          tkptr->FtpWildPtr[0] ? " " : "", tkptr->FtpWildPtr);
         break;

      case PROXY_FTP_FILE_SYSTEM_UNIX :
         ProxyFtpCommand (tkptr, true, "LIST!AZ!AZ",
                          tkptr->FtpWildPtr[0] ? " " : "", tkptr->FtpWildPtr);
         break;

      case PROXY_FTP_FILE_SYSTEM_VMS :
         if (tkptr->FtpWildPtr[0])
         {
            /* need to constrain or allow version numbers */
            cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
            while (cptr > tkptr->FtpFilePathPtr &&
                   *cptr != '/' && *cptr != ';') cptr--;
            if (*cptr == ';')
               ProxyFtpCommand (tkptr, true, "LIST !AZ", tkptr->FtpWildPtr);
            else
               ProxyFtpCommand (tkptr, true, "LIST !AZ;0", tkptr->FtpWildPtr);
         }
         else
            ProxyFtpCommand (tkptr, true, "LIST *.*;0");
         break;

      default :
         ProxyFtpCommand (tkptr, true, "NLIST!AZ!AZ",
                          tkptr->FtpWildPtr[0] ? " " : "", tkptr->FtpWildPtr);
   }

   /* reset the IO status block for the first read */
   tkptr->FtpDataReadIOsb.Status = tkptr->FtpDataReadIOsb.Count = 0;
}

/****************************************************************************/
/*
Received a chunk of a directory lsiting from the remote server.  After checking
the netwrok read status and it being OK add this to the dynamically allocated
listing buffer and then read some more.
*/

ProxyFtpListReceive (PROXY_TASK *tkptr)

{
   int  status;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListReceive() !&S !UL",
                 tkptr->FtpDataReadIOsb.Status, tkptr->FtpDataReadIOsb.Count);

   rqptr = tkptr->RequestPtr;

   if (tkptr->FtpDataReadIOsb.Status == SS$_LINKDISCON)
   {
      /* transfer is complete (or aborted!) */
      ProxyFtpDataCloseSocket (tkptr);
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   if (!tkptr->FtpDataReadIOsb.Status)
   {
      /* first time the function has been called */
      if (!tkptr->ResponseBufferPtr)
      {
         tkptr->ResponseBufferSize = ProxyReadBufferSize;
         tkptr->ResponseBufferPtr = VmGetHeap (rqptr, ProxyReadBufferSize);
      }
      tkptr->ResponseBufferCount = 0;
      tkptr->ResponseBufferRemaining = tkptr->ResponseBufferSize - 1;
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr;
      /* plus, fudge the first IO status block appropriately */
      tkptr->FtpDataReadIOsb.Status = SS$_NORMAL;
   }

   if (VMSnok (tkptr->FtpDataReadIOsb.Status))
   {
      /* check the previous read status */
      rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
      rqptr->rqResponse.HttpStatus = 502;
      ErrorVmsStatus (rqptr, tkptr->FtpDataReadIOsb.Status, FI_LI);
      tkptr->FtpState = PROXY_FTP_STATE_ABORT;
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   tkptr->ResponseBufferCount += tkptr->FtpDataReadIOsb.Count;
   tkptr->ResponseBufferCurrentPtr += tkptr->FtpDataReadIOsb.Count;
   tkptr->ResponseBufferRemaining -= tkptr->FtpDataReadIOsb.Count;

   if (tkptr->ResponseBufferRemaining < PROXY_FTP_LIST_LOW_BUFFER)
   {
      tkptr->ResponseBufferSize += ProxyReadBufferSize; 
      tkptr->ResponseBufferRemaining += ProxyReadBufferSize;
      tkptr->ResponseBufferPtr =
          VmReallocHeap (rqptr, tkptr->ResponseBufferPtr,
                         tkptr->ResponseBufferSize, FI_LI);
      tkptr->ResponseBufferCurrentPtr = tkptr->ResponseBufferPtr +
                                        tkptr->ResponseBufferCount;
   }

   *tkptr->ResponseBufferCurrentPtr = '\0';

   ProxyFtpDataReadRaw (tkptr, &ProxyFtpListReceive,
                        tkptr->ResponseBufferCurrentPtr,
                        tkptr->ResponseBufferRemaining);
}

/****************************************************************************/
/*
The directory listing has been received and is currently stored in memory
pointed to by ->ResponseBufferPtr, is ->ResponseBufferCount in size, and is
null-terminated.  Format this according to the type of file system.  Write this
to the proxy client.
*/

ProxyFtpListProcess (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListProcess()");

   rqptr = tkptr->RequestPtr;

   if (tkptr->FtpFileSystem == PROXY_FTP_FILE_SYSTEM_UNIX)
   {
      /* first let's double-check it's not DOS masquerading as Unix */
      cptr = tkptr->ResponseBufferPtr;
      if (isdigit(cptr[0]) && isdigit(cptr[1]) &&
          (cptr[2] == '-' || cptr[2] == '/'))
      {
         /* hmmm, looks suspiciously like a ... */
         while (!ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
         while (ISLWS(*cptr)) cptr++;
         if (isdigit(cptr[0]) && isdigit(cptr[1]) && cptr[2] == ':' &&
             isdigit(cptr[3]) && isdigit(cptr[4]))
         {
            /* ... DOS date and time (dd/mm/yy hh:mm) */
            tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_DOS;
            tkptr->FtpFileSystemPtr = "DOS";
            /* (and nothing like Unix file permissions!) */
            InstanceMutexLock (INSTANCE_MUTEX_HTTPD);
            ProxyAccountingPtr->FtpUnixCount--;
            ProxyAccountingPtr->FtpDosCount++;
            InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD);
         }
      }
   }

   if (*(cptr = rqptr->rqHeader.QueryStringPtr))
   {
      if (ProxyFtpInQueryString (cptr, "dos"))
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_DOS;
      else
      if (ProxyFtpInQueryString (cptr, "unix"))
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_UNIX;
      else
      if (ProxyFtpInQueryString (cptr, "vms"))
         tkptr->FtpFileSystem = PROXY_FTP_FILE_SYSTEM_VMS;
      else
      if (ProxyFtpInQueryString (cptr, "list"))
      {
         rqptr->rqResponse.PreExpired = PRE_EXPIRE_FTP_PROXY;
         ResponseHeader (rqptr, 200, "text/plain", 0, NULL, NULL);
         NetWrite (rqptr, &ProxyFtpListWriteAst,
                   tkptr->ResponseBufferPtr, tkptr->ResponseBufferCount);
         return;
      }
      if (ProxyFtpInQueryString (cptr, "alt"))
         tkptr->FtpListAlt = true;
      if (ProxyFtpInQueryString (cptr, "hide"))
         tkptr->FtpListHide = true;
      if (ProxyFtpInQueryString (cptr, "raw"))
         tkptr->FtpListRaw = true;
   }

   switch (tkptr->FtpFileSystem)
   {
      case PROXY_FTP_FILE_SYSTEM_DOS :
         ProxyFtpListProcessDOS (tkptr);
         break;
      case PROXY_FTP_FILE_SYSTEM_UNIX :
         ProxyFtpListProcessUnix (tkptr);
         break;
      case PROXY_FTP_FILE_SYSTEM_VMS :
         ProxyFtpListProcessVMS (tkptr);
         break;
      default :
         /* unknown format */
         rqptr->rqResponse.PreExpired = PRE_EXPIRE_FTP_PROXY;
         ResponseHeader (rqptr, 200, "text/plain", 0, NULL, NULL);
         NetWrite (rqptr, &ProxyFtpListWriteAst,
                   tkptr->ResponseBufferPtr, tkptr->ResponseBufferCount);
         return;
   }

   ProxyFtpListOutput (tkptr);

   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
************
*** NOTE ***  This function takes a pointer to a request!!!
************
*/

ProxyFtpListWriteAst (REQUEST_STRUCT *rqptr)

{
   PROXY_TASK  *tkptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (rqptr, FI_LI, WATCH_MOD_PROXY, "ProxyFtpListWriteAst()");

   tkptr = rqptr->ProxyTaskPtr; 

   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Process the native DOS directory listing into the standard format used by
ProxyFtpListOutput().
*/

ProxyFtpListProcessDOS (PROXY_TASK *tkptr)

{
#define FORMAT_DOS_DATE 1

   static char  *MonthName [] = { NULL, "Jan","Feb","Mar","Apr","May","Jun",
                                  "Jul","Aug","Sep","Oct","Nov","Dec" };

   BOOL  IsDirectory,
         MonthDayOrdering;
   int  DateDay,
        DateHour,
        DateMinute,
        DateMonth,
        DateYear;
   char  *cptr, *sptr, *tptr, *zptr;
   char  FileDate [64],
         FileName [256],
         FileSize [64];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListProcessDOS()");

   rqptr = tkptr->RequestPtr;

#if FORMAT_DOS_DATE

   if (!tkptr->FtpListRaw)
   {
      MonthDayOrdering = false;
      cptr = tkptr->ResponseBufferPtr;
      while (*cptr)
      {
         if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
            WatchDataFormatted ("-!&L\n", cptr);

         /* skip leading white-space, blank lines, etc. */
         while (ISLWS(*cptr)) cptr++;
         if (!*cptr) break;
         if (EOL(*cptr))
         {
            while (*cptr && EOL(*cptr)) cptr++;
            continue;
         }

         if (isdigit(cptr[0]) && isdigit(cptr[1]) &&
             (cptr[2] == '-' || cptr[2] == '/'))
         {
            DateDay = atoi(cptr);
            cptr += 3;
            DateMonth = atoi(cptr);
            if (DateDay > 0 && DateDay <= 31 &&
                DateMonth > 0 && DateMonth <= 31 &&
                DateMonth > 12 && DateMonth <= 31)
            {
               MonthDayOrdering = true;
               break;
            }
         }

         /* skip to start of next line */
         while (NOTEOL(*cptr)) cptr++;
         while (*cptr && EOL(*cptr)) cptr++;
      }
   }

#endif /* FORMAT_DOS_DATE */

   tkptr->ResponseBufferCount = 0;
   cptr = tkptr->ResponseBufferPtr;
   while (*cptr)
   {
      if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         WatchDataFormatted ("-!&L\n", cptr);

      /* skip leading white-space, blank lines, etc. */
      while (ISLWS(*cptr)) cptr++;
      if (!*cptr) break;
      if (EOL(*cptr))
      {
         while (*cptr && EOL(*cptr)) cptr++;
         continue;
      }

      /* file date and time */
      zptr = (sptr = FileDate) + sizeof(FileDate)-1;
      while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
         *sptr++ = *cptr++;
      while (ISLWS(*cptr) && sptr < zptr) *sptr++ = *cptr++;
      while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
         *sptr++ = *cptr++;
      *sptr = '\0';

      /* absorb intervening white-space */
      while (ISLWS(*cptr)) cptr++;

      if (*cptr == '<' && !memcmp(cptr, "<DIR>", 5))
      {
         IsDirectory = true;
         FileSize[0] = '\0';
         while (!ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
         while (ISLWS(*cptr)) cptr++;
      }
      else
      {
         IsDirectory = false;
         zptr = (sptr = FileSize) + sizeof(FileSize)-1;
         while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
            *sptr++ = *cptr++;
         *sptr = '\0';
      }

      /* absorb intervening white-space */
      while (ISLWS(*cptr)) cptr++;

      zptr = (sptr = FileName) + sizeof(FileName)-1;
      /* allow for the likes of "Copy of file.name" */
      while (NOTEOL(*cptr) && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';

      /* skip to start of next line */
      while (NOTEOL(*cptr)) cptr++;
      while (*cptr && EOL(*cptr)) cptr++;

      /* if all components not present then just ignore */
      if (!FileName[0] || !FileDate[0] || (!FileSize[0] && !IsDirectory))
         continue;

      /* try to eliminate other lines ... file size really look like one? */
      for (sptr = FileSize; isdigit(*sptr) || *sptr == ','; sptr++);
      if (*sptr) continue;

      /* try to eliminate other lines ... file date really look like one? */
      sptr = FileDate;
      if (!(isdigit(sptr[0]) && isdigit(sptr[1]) &&
            (sptr[2] == '/' || sptr[2] == '-'))) continue;

#if FORMAT_DOS_DATE

      if (!tkptr->FtpListRaw)
      {
         sptr = FileDate;
         if (MonthDayOrdering)
         {
            DateMonth = atoi(sptr);
            sptr += 3;
            DateDay = atoi(sptr);
         }
         else
         {
            DateDay = atoi(sptr);
            sptr += 3;
            DateMonth = atoi(sptr);
         }
         sptr += 3;
         DateYear = atoi(sptr);
         if (DateYear <= 99)
         {
            DateYear += 1900;
            if (DateYear < 1970) DateYear += 100;
         }
         while (*sptr && isdigit(*sptr)) sptr++;
         while (*sptr && ISLWS(*sptr)) sptr++;
         DateHour = atoi(sptr);
         sptr += 3;
         DateMinute = atoi(sptr);
         sptr += 2;
         if (tolower(*sptr) == 'p')
         {
            /* allow for "12:00PM" i.e. midday */
            if (DateHour <= 11) DateHour += 12;
         }
         else
         /* if it's not PM or AM then force an error */
         if (tolower(*sptr) != 'a')
            DateHour = -1;

         if (DateMinute >= 0 && DateMinute < 59 &&
             DateHour >= 0 && DateHour <= 23 &&
             DateDay > 0 && DateDay <= 31 &&
             DateMonth > 0 && DateMonth <= 12 &&
             DateYear > 1970 && DateYear <= 2099)
         { 
            /* seems to make sense, reformat */
            if (DateYear == rqptr->rqTime.VmsVector[0])
               WriteFao (FileDate, sizeof(FileDate), NULL,
                         "!AZ !AZ!UL !2ZL:!2ZL",
                         MonthName[DateMonth],
                         DateDay < 10 ? " " : "", DateDay,
                         DateHour, DateMinute); 
            else
               WriteFao (FileDate, sizeof(FileDate), NULL,
                         "!AZ !AZ!UL  !UL",
                         MonthName[DateMonth],
                         DateDay < 10 ? " " : "", DateDay,
                         DateYear); 
         }
      }

#endif /* FORMAT_DOS_DATE */

      sptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferCount;
      zptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferSize - 1;

      tptr = FileName;
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = '\t';
      tptr = FileSize;
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = '\t';
      tptr = FileDate;
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = '\n';

      if (sptr >= cptr)
      {
         tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount = 0] = '\0';
         rqptr->rqResponse.HttpStatus = 500;
         ErrorGeneral (rqptr, ErrorSanityCheck, FI_LI);
         return;
      }

      *sptr = '\0';
      tkptr->ResponseBufferCount = sptr - tkptr->ResponseBufferPtr;
   }

   tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount] = '\0';

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", tkptr->ResponseBufferPtr);
}

/****************************************************************************/
/*
Process the native Unix directory listing into the standard format used by
ProxyFtpListOutput().
*/

ProxyFtpListProcessUnix (PROXY_TASK *tkptr)

{
#define MAX_FIELDS 16
#define SIZE_FIELDS 128

   static char  *MonthName [] = { "Jan","Feb","Mar","Apr","May","Jun",
                                  "Jul","Aug","Sep","Oct","Nov","Dec" };

   BOOL  IsDirectory;
   int  idx, status,
        FieldCount,
        FileNameLength,
        MonthField;
   char  *cptr, *sptr, *tptr, *zptr;
   char  Fields [MAX_FIELDS][SIZE_FIELDS];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListProcessUnix()");

   rqptr = tkptr->RequestPtr;

   tkptr->ResponseBufferCount = 0;
   cptr = tkptr->ResponseBufferPtr;
   while (*cptr)
   {
      if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         WatchDataFormatted ("-!&L\n", cptr);

      /* skip leading white-space, blank lines, etc. */
      while (ISLWS(*cptr)) cptr++;
      if (!*cptr) break;
      if (EOL(*cptr))
      {
         while (*cptr && EOL(*cptr)) cptr++;
         continue;
      }

      FieldCount = 0;
      while (*cptr && NOTEOL(*cptr))
      {
         if (FieldCount < MAX_FIELDS)
         {
            zptr = (sptr = Fields[FieldCount++]) + SIZE_FIELDS-1;
            while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
               *sptr++ = *cptr++;
            *sptr++ = '\0';
         }
         while (*cptr && !ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
         while (ISLWS(*cptr)) cptr++;
      }
      if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         for (idx = 1; idx < FieldCount; idx++)
            WatchDataFormatted ("!UL !&Z\n", idx, Fields[idx]);

      /* skip to start of next line */
      while (NOTEOL(*cptr)) cptr++;
      while (*cptr && EOL(*cptr)) cptr++;

      /* won't make any sense at all unless there are at least six fields */
      if (FieldCount < 6) continue;

      /* find field containing month name */
      for (MonthField = 1; MonthField < FieldCount; MonthField++)
      {
         for (idx = 0; idx < 12; idx++)
             if (strsame (Fields[MonthField], MonthName[idx], 3))
                break;
         if (idx < 12) break;
      }

      /* doesn't make sense if it doesn't contain a month name somewhere */
      if (MonthField >= FieldCount) continue;

      /* dot and double-dot directories are ignored */
      tptr = Fields[MonthField+3];
      if (*(unsigned short*)tptr == '.\0') continue;
      if (*(unsigned short*)tptr == './') continue;
      if (*(unsigned short*)tptr == '..') continue;

      if (Fields[0][0] == '-')
         IsDirectory = false;
      else
      if (Fields[0][0] == 'd')
         IsDirectory = true;
      else
      if (Fields[0][0] == 'l')
      {
         /* a symbolic link, check if it looks like a file (i.e. has type) */
         for (tptr = Fields[MonthField+5]; *tptr && *tptr != '.'; tptr++);
         if (!*tptr) IsDirectory = true;
      }
      else
         /* doesn't make sense, ignore */
         continue;

      sptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferCount;
      zptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferSize - 1;

      /* file name */
      tptr = Fields[MonthField+3];
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = '\t';

      /* file size */
      if (!IsDirectory)
      {
         tptr = Fields[MonthField-1];
         while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      }
      if (sptr < zptr) *sptr++ = '\t';

      /* file date/time */
      tptr = Fields[MonthField];
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = ' ';
      if (!Fields[MonthField+1][1] && sptr < zptr)
         *sptr++ = ' ';
      else
      if (Fields[MonthField+1][0] == '0')
         Fields[MonthField+1][0] = ' ';
      tptr = Fields[MonthField+1];
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = ' ';
      if (strlen(Fields[MonthField+2]) == 4 && sptr < zptr) *sptr++ = ' ';
      tptr = Fields[MonthField+2];
      while (*tptr && sptr < zptr) *sptr++ = *tptr++;
      if (sptr < zptr) *sptr++ = '\n';

      if (sptr >= cptr)
      {
         tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount = 0] = '\0';
         rqptr->rqResponse.HttpStatus = 500;
         ErrorGeneral (rqptr, ErrorSanityCheck, FI_LI);
         return;
      }

      *sptr = '\0';
      tkptr->ResponseBufferCount = sptr - tkptr->ResponseBufferPtr;
   }

   tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount] = '\0';

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", tkptr->ResponseBufferPtr);

#undef MAX_FIELDS 
}

/****************************************************************************/
/*
Process the native VMS directory listing into the standard format used by
ProxyFtpListOutput().  Needless-to-say this is the more complicated of the
reformatting functions.  To ease integration with non-browser applications it
attempts to make the standard VMS listing look more generic (particularly as
far as date/time goes).  If there is a version component in the path it then
reverts back displaying version infomation, size and date/time in a more
VMS-like fashion.
*/

ProxyFtpListProcessVMS (PROXY_TASK *tkptr)

{
   BOOL  IsDirectory,
         LooksOk;
   int  year,
        FileSizeBytes;
   unsigned short  Length;
   char  *cptr, *sptr, *tptr, *zptr;
   char  DateDay [4],
         DateMonth [4],
         DateTime [16],
         DateYear [8],
         FileName [128],
         FileSize [32];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListProcessVMS()");

   rqptr = tkptr->RequestPtr;

   cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
   while (cptr > tkptr->FtpFilePathPtr && *cptr != '/' && *cptr != ';') cptr--;
   if (*cptr == ';')
      tkptr->FtpHasVersion = true;
   else
      tkptr->FtpHasVersion = false;

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", tkptr->ResponseBufferPtr);

   tkptr->ResponseBufferCount = 0;
   cptr = tkptr->ResponseBufferPtr;
   while (*cptr)
   {
      if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         WatchDataFormatted ("-!&L\n", cptr);

      /* skip leading white-space, blank lines, etc. */
      while (ISLWS(*cptr)) cptr++;
      if (!*cptr) break;
      if (EOL(*cptr))
      {
         while (*cptr && EOL(*cptr)) cptr++;
         continue;
      }

      LooksOk = true;

      zptr = (sptr = FileName) + sizeof(FileName)-1;
      if (tkptr->FtpHasVersion || tkptr->FtpListRaw)
      {
         while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
            *sptr++ = *cptr++;
      }
      else
      {
         while (!ISLWS(*cptr) && *cptr != ';' && NOTEOL(*cptr) && sptr < zptr)
            *sptr++ = *cptr++;
         if (*cptr == ';')
         {
            /* skip version number in file specification */
            while (!ISLWS(*cptr) && NOTEOL(*cptr)) cptr++;
         }
      }
      *sptr = '\0';
      if (!FileName[0]) LooksOk = false;

      while (sptr > FileName && *sptr != '.') sptr--;
      if (strsame (sptr, ".DIR", -1) ||
          strsame (sptr, ".DIR;", 5))
      {
         /* terminate to eliminate the ".DIR" */
         *sptr = '\0';
         IsDirectory = true;
      }
      else
         IsDirectory = false;

      /* absorb white-space after filename and before size */
      while (ISLWS(*cptr)) cptr++;

      if (EOL(*cptr))
      {
         /* file name too long for one line, try size/date on next line */
         while (NOTEOL(*cptr)) cptr++;
         while (*cptr && EOL(*cptr)) cptr++;
         if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
            WatchDataFormatted ("+!&L\n", cptr);
         /* absorb leading white-space */
         while (ISLWS(*cptr)) cptr++;
      }

      zptr = (sptr = FileSize) + sizeof(FileSize)-1;
      while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < zptr)
      {
         if (!isdigit(*cptr) && *cptr != '/') LooksOk = false;
         *sptr++ = *cptr++;
      }
      /* a null file size indicates it's a directory */
      if (IsDirectory)
         sptr = FileSize;
      else
      if (!(tkptr->FtpHasVersion || tkptr->FtpListRaw))
      {
         /* not a VMS specification, convert from blocks to bytes */
         *sptr = '\0';
         FileSizeBytes = atoi(FileSize) * 512;
         WriteFao (FileSize, sizeof(FileSize), &Length, "!UL", FileSizeBytes);
         sptr = FileSize + Length;
      }
      *sptr = '\0';
      if (!FileSize[0] && !IsDirectory) LooksOk = false;

      /* absorb white-space between size and date */
      while (ISLWS(*cptr)) cptr++;

      /* note that we are retrieving the date into working storage here */
      sptr = DateDay;
      if (cptr[1] == '-') *sptr++ = ' ';
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && *cptr != '-') *sptr++ = *cptr++;
      *sptr = '\0';
      if (*cptr == '-')
         cptr++;
      else
         LooksOk = false;
      sptr = DateMonth;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      *sptr = '\0';
      if (*cptr == '-')
         cptr++;
      else
         LooksOk = false;
      sptr = DateYear;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      if (*cptr && NOTEOL(*cptr)) *sptr++ = *cptr++;
      *sptr = '\0';
      if (*cptr == ' ')
         cptr++;
      else
         LooksOk = false;
      tptr = (sptr = DateTime) + sizeof(DateTime)-1;
      while (!ISLWS(*cptr) && NOTEOL(*cptr) && sptr < tptr)
      {
         if (!isdigit(*cptr) && *cptr != ':' && *cptr != '.') LooksOk = false;
         *sptr++ = *cptr++;
      }
      *sptr = '\0';

      /* if all components do not appear present */
      if (!(DateDay[0] && DateMonth[0] &&
            DateYear[0] && DateTime[0])) LooksOk = false;

      /* skip to start of next line */
      while (NOTEOL(*cptr)) cptr++;
      while (*cptr && EOL(*cptr)) cptr++;

      /* if it doesn't look right then just ignore */
      if (!LooksOk) continue;

      sptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferCount;
      zptr = tkptr->ResponseBufferPtr + tkptr->ResponseBufferSize - 1;

      for (tptr = FileName; *tptr && sptr < zptr; *sptr++ = *tptr++);
      if (sptr < zptr) *sptr++ = '\t';

      for (tptr = FileSize; *tptr && sptr < zptr; *sptr++ = *tptr++);
      if (sptr < zptr) *sptr++ = '\t';

      if (tkptr->FtpHasVersion || tkptr->FtpListRaw)
      {
         tptr = DateDay;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = '-';
         tptr = DateMonth;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = '-';
         tptr = DateYear;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = ' ';
         tptr = DateTime;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
      }
      else
      {
         tptr = DateMonth;
         if (*tptr && sptr < zptr) *sptr++ = *tptr++;
         if (*tptr && sptr < zptr) *sptr++ = tolower(*tptr++);
         if (*tptr && sptr < zptr) *sptr++ = tolower(*tptr++);
         if (sptr < zptr) *sptr++ = ' ';
         if (!DateDay[1] && sptr < zptr) *sptr++ = ' ';
         for (tptr = DateDay; *tptr && sptr < zptr; *sptr++ = *tptr++);
         if (sptr < zptr) *sptr++ = ' ';
         year = atoi(DateYear);
         if (year == rqptr->rqTime.VmsVector[0])
         {
            tptr = DateTime;
            if (sptr < zptr) *sptr++ = *tptr++;
         }
         else
         {
            tptr = DateYear;
            if (sptr < zptr) *sptr++ = ' ';
         }
         if (sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = *tptr++;
         if (sptr < zptr) *sptr++ = *tptr++;
      }
      if (sptr < zptr) *sptr++ = '\n';

      if (sptr >= cptr)
      {
         tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount = 0] = '\0';
         rqptr->rqResponse.HttpStatus = 500;
         ErrorGeneral (rqptr, ErrorSanityCheck, FI_LI);
         return;
      }
      
      *sptr = '\0';
      tkptr->ResponseBufferCount = sptr - tkptr->ResponseBufferPtr;
   }

   tkptr->ResponseBufferPtr[tkptr->ResponseBufferCount] = '\0';

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchDataFormatted ("!&Z\n", tkptr->ResponseBufferPtr);
}

/****************************************************************************/
/*
Output the reformatted directory listing, directories first, then files.
The reformatted version is "<filename><TAB><size><TAB><date><LF>" as in the
following file example "EXAMPLE.TXT\t1560\tOct 28  2001\n" and directory
example "EXAMPLE\t\tOct 28  2001\n".  As can be seen from the example a
directory is indicated by an empty size field.
*/

ProxyFtpListOutput (PROXY_TASK *tkptr)

{
#define NAME_WIDTH 30
#define SIZE_WIDTH 15

   static char  DirFao [] =
"!AZ  <A HREF=\"!#&;AZ/!&;AZ!AZ\">!#&;AZ/</A>!#*   !#*  !#AZ\n"; 
   static char  FileFao [] =
"!AZ!&@  <A HREF=\"!#&;AZ!AZ\">!#&;AZ</A>!#*   !&@  !#AZ\n"; 

   static char  BeginPage1Fao [] =
"<HTML>\n\
<HEAD>\n\
<META NAME=\"SYST\" VALUE=\"!AZ\">\n\
<META NAME=\"CWD\" VALUE=\"!AZ\">\n\
<META NAME=\"file\" VALUE=\"!AZ\">\n\
<TITLE>!AZ!AZ//!AZ!AZ</TITLE>\n\
</HEAD>\n\
!&@\
!&@",
                BeginPage2Fao [] =
"!&@\
!AZ\
<PRE>!AZ!AZ  !AZ!#*   !#* !AZ  !AZ!AZ<HR SIZE=2 NOSHADE>\n\
!&@";

   static char  EndPageFao [] =
"<HR SIZE=2 NOSHADE></PRE>\n\
!&@\
</BODY>\n\
</HTML>\n";

   int  status,
        DirCount;
   unsigned short  Length,
                   FileDateLength,
                   FileNameLength,
                   FileSizeLength;
   unsigned long  *vecptr;
   unsigned long  FaoVector [32];
   char  *cptr, *sptr,
         *AltTextPtr,
         *FileDatePtr,
         *FileNamePtr,
         *FileSizePtr,
         *IconUriPtr,
         *IndexOfPtr;
   char  BlankIconImg [256],
         CommaSize [32],
         IconImg [256],
         PrevContentType [128];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpListOutput()");

   rqptr = tkptr->RequestPtr;
   BlankIconImg[0] = IconImg[0] = PrevContentType[0] = '\0';

   ConfigIconFor (ConfigContentTypeBlank, &IconUriPtr, &AltTextPtr);
   WriteFao (BlankIconImg, sizeof(BlankIconImg), NULL,
             "<IMG SRC=\"!AZ//!AZ!AZ\" ALIGN=top ALT=\"!AZ\">",
             rqptr->ServicePtr->RequestSchemeNamePtr,
             rqptr->ServicePtr->ServerHostPort,
             IconUriPtr, AltTextPtr);

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_FTP_PROXY;
   ResponseHeader (rqptr, 200, "text/html", -1, NULL, NULL);

   IndexOfPtr = MsgFor(rqptr,MSG_PROXY_FTP_INDEX_OF);

   sptr = MsgFor(rqptr,MSG_DIR_COLUMN_HEADINGS);
   cptr = VmGetHeap (rqptr, strlen(sptr));
   strcpy (cptr, sptr);
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') cptr++;
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') cptr++;
   FileNamePtr = cptr;
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') *cptr++ = '\0';
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') cptr++;
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') cptr++;
   FileDatePtr = cptr;
   while (*cptr && *cptr != '|') cptr++;
   if (*cptr = '|') *cptr++ = '\0';
   FileSizePtr = cptr;

   vecptr = FaoVector;

   *vecptr++ = tkptr->FtpSYST;
   *vecptr++ = tkptr->FtpCWD;
   *vecptr++ = tkptr->FtpFileSystemPtr;

   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
   {
      *vecptr++ = "";
      *vecptr++ = "";
   }
   else
   {
      *vecptr++ = IndexOfPtr;
      *vecptr++ = " ";
   }
   if (rqptr->rqHeader.HostPtr &&
       rqptr->rqHeader.HostPtr[0])
      *vecptr++ = rqptr->rqHeader.HostPtr;
   else
      *vecptr++ = rqptr->ServicePtr->ServerHostPort;
   *vecptr++ = tkptr->FtpFilePathPtr;

   if (rqptr->rqPathSet.HtmlBodyTagPtr)
   {
      /* <body..> */
      if (rqptr->rqPathSet.HtmlBodyTagPtr[0] == '<')
         *vecptr++ = "!AZ\n";
      else
         *vecptr++ = "<BODY!&+AZ>\n";
      *vecptr++ = rqptr->rqPathSet.HtmlBodyTagPtr;
   }
   else
   {
      *vecptr++ = "!AZ\n";
      *vecptr++ = Config.cfDir.BodyTag;
   }

   if (rqptr->rqPathSet.HtmlHeaderPtr ||
       rqptr->rqPathSet.HtmlHeaderTagPtr)
   {
      if (rqptr->rqPathSet.HtmlHeaderTagPtr &&
          rqptr->rqPathSet.HtmlHeaderTagPtr[0] == '<')
         *vecptr++ = "!AZ\n!&/AZ";
      else
         *vecptr++ =
"<TABLE CELLPADDING=5 CELLSPACING=0 BORDER=0 WIDTH=100%><TR><TD!&+AZ>\n!&/AZ";
      *vecptr++ = rqptr->rqPathSet.HtmlHeaderTagPtr;
      *vecptr++ = rqptr->rqPathSet.HtmlHeaderPtr;
   }
   else
      *vecptr++ = "";

   status = NetWriteFaol (rqptr, BeginPage1Fao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   ProxyFtpIndexOf (tkptr, IndexOfPtr);

   vecptr = FaoVector;

   if (tkptr->Ftp230Ptr)
   {
      *vecptr++ = "<PRE>!&;AZ</PRE>\n";
      *vecptr++ = tkptr->Ftp230Ptr;
   }
   else
      *vecptr++ = "";

   if (rqptr->rqPathSet.HtmlHeaderPtr ||
       rqptr->rqPathSet.HtmlHeaderTagPtr)
      *vecptr++ = "</TD></TR></TABLE>\n";
   else
      *vecptr++ = "";

   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
      *vecptr++ = "<B>";
   else
      *vecptr++ = "";
   *vecptr++ = BlankIconImg;
   FileNameLength = strlen(FileNamePtr);
   *vecptr++ = FileNamePtr;
   *vecptr++ = FileNameLength >= NAME_WIDTH ?
                  0 : NAME_WIDTH - FileNameLength;
   FileSizeLength = strlen(FileSizePtr);
   *vecptr++ = FileSizeLength >= SIZE_WIDTH ?
                  0 : SIZE_WIDTH - FileSizeLength;
   *vecptr++ = FileSizePtr;
   *vecptr++ = FileDatePtr;
   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
      *vecptr++ = "</B>";
   else
      *vecptr++ = "";

   DirCount = 0;
   for (cptr = tkptr->FtpFilePathPtr + 1; *cptr; *cptr++)
      if (*cptr == '/') DirCount++;
   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_ORIGINAL && DirCount)
   {
      ConfigIconFor (ConfigContentTypeDir, &IconUriPtr, &AltTextPtr);
      WriteFao (IconImg, sizeof(IconImg), NULL,
                "<IMG SRC=\"!AZ//!AZ!AZ\" ALIGN=top ALT=\"!AZ\">",
                rqptr->ServicePtr->RequestSchemeNamePtr,
                rqptr->ServicePtr->ServerHostPort,
                IconUriPtr, AltTextPtr);
      *vecptr++ = "!AZ  <A HREF=\"../!&;AZ!AZ\">../</A>\n";
      *vecptr++ = IconImg;
      *vecptr++ = tkptr->FtpWildPtr;
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
   }
   else
      *vecptr++ = "";

   status = NetWriteFaol (rqptr, BeginPage2Fao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   /***************/
   /* directories */
   /***************/

   cptr = tkptr->ResponseBufferPtr;
   while (*cptr)
   {
      FileNamePtr = cptr;
      while (*cptr && *cptr != '\t') cptr++;
      FileNameLength = cptr - FileNamePtr;
      if (*cptr) cptr++;
      FileSizePtr = cptr;
      while (*cptr && *cptr != '\t') cptr++;
      FileSizeLength = cptr - FileSizePtr;
      if (*cptr) cptr++;
      FileDatePtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      FileDateLength = cptr - FileDatePtr;
      if (*cptr) cptr++;

      /* empty file size indicates a directory */
      if (*FileSizePtr != '\t') continue;

      if (tkptr->FtpListHide && *FileNamePtr == '.') continue;

      if (!IconImg[0])
      {
         ConfigIconFor (ConfigContentTypeDir, &IconUriPtr, &AltTextPtr);
         WriteFao (IconImg, sizeof(IconImg), NULL,
                   "<IMG SRC=\"!AZ//!AZ!AZ\" ALIGN=top BORDER=0 ALT=\"!AZ\">",
                   rqptr->ServicePtr->RequestSchemeNamePtr,
                   rqptr->ServicePtr->ServerHostPort,
                   IconUriPtr, AltTextPtr);
      }

      vecptr = FaoVector;

      *vecptr++ = IconImg;
      *vecptr++ = FileNameLength;
      *vecptr++ = FileNamePtr;
      *vecptr++ = tkptr->FtpWildPtr;
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
      *vecptr++ = FileNameLength;
      *vecptr++ = FileNamePtr;
      *vecptr++ = FileNameLength >= NAME_WIDTH ?
                     0 : NAME_WIDTH - FileNameLength;
      *vecptr++ = SIZE_WIDTH;
      *vecptr++ = FileDateLength;
      *vecptr++ = FileDatePtr;

      status = NetWriteFaol (rqptr, DirFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

      DirCount++;
   }

   /*********/
   /* files */
   /*********/

   cptr = tkptr->ResponseBufferPtr;
   while (*cptr)
   {
      FileNamePtr = cptr;
      while (*cptr && *cptr != '\t') cptr++;
      FileNameLength = cptr - FileNamePtr;
      if (*cptr) cptr++;
      FileSizePtr = cptr;
      while (*cptr && *cptr != '\t') cptr++;
      FileSizeLength = cptr - FileSizePtr;
      if (*cptr) cptr++;
      FileDatePtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      FileDateLength = cptr - FileDatePtr;
      if (*cptr) cptr++;

      /* empty file size indicates a directory */
      if (*FileSizePtr == '\t') continue;

      if (tkptr->FtpListHide && *FileNamePtr == '.') continue;

      FileNamePtr[FileNameLength] = '\0';
      for (sptr = FileNamePtr + FileNameLength;
           sptr > FileNamePtr && *sptr != '.';
           sptr--);
      if (*sptr == '.')
         sptr = ConfigContentType (NULL, sptr);
      else
         sptr = ConfigDefaultFileContentType;
      FileNamePtr[FileNameLength] = '\t';

      if (!PrevContentType[0] || !strsame (sptr, PrevContentType, -1))
      {
         strzcpy (PrevContentType, sptr, sizeof(PrevContentType));
         ConfigIconFor (sptr, &IconUriPtr, &AltTextPtr);
         WriteFao (IconImg, sizeof(IconImg), NULL,
                   "<IMG SRC=\"!AZ//!AZ!AZ\" ALIGN=top BORDER=0 ALT=\"!AZ\">",
                   rqptr->ServicePtr->RequestSchemeNamePtr,
                   rqptr->ServicePtr->ServerHostPort,
                   IconUriPtr, AltTextPtr);
      }

      vecptr = FaoVector;

      if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_ORIGINAL && DirCount)
      {
         *vecptr++ = "\n";
         DirCount = 0;
      }
      else
         *vecptr++ = "";

      if (tkptr->FtpListAlt)
      {
         *vecptr++ = "<A HREF=\"!#&%AZ!AZ!AZ!AZ\">!AZ</A>";
         *vecptr++ = FileNameLength;
         *vecptr++ = FileNamePtr;
         *vecptr++ = tkptr->RequestUriQueryStringPtr;
         *vecptr++ = tkptr->RequestUriQueryStringPtr[0] ? "" : "?";
         *vecptr++ = strsame(sptr, "text/", 5) ? "+octet+image" : "+text+ascii";
         *vecptr++ = IconImg;
      }
      else
         *vecptr++ = IconImg;

      *vecptr++ = FileNameLength;
      *vecptr++ = FileNamePtr;
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
      *vecptr++ = FileNameLength;
      *vecptr++ = FileNamePtr;
      *vecptr++ = FileNameLength >= NAME_WIDTH ?
                     0 : NAME_WIDTH - FileNameLength;

      for (sptr = FileSizePtr;
           *sptr && *sptr != '\t' && *sptr != ',';
           sptr++);
      if (!(tkptr->FtpHasVersion || tkptr->FtpListRaw) && *sptr != ',')
      {
         FileSizePtr[FileSizeLength] = '\0';
         WriteFao (CommaSize, sizeof(CommaSize), &Length, "!&,AZ", FileSizePtr);
         FileSizePtr[FileSizeLength] = '\t';
         *vecptr++ = "!#* !AZ";
         *vecptr++ = Length >= SIZE_WIDTH ? 0 : SIZE_WIDTH - Length;
         *vecptr++ = CommaSize;
      }
      else
      {
         *vecptr++ = "!#* !#AZ";
         *vecptr++ = FileSizeLength >= SIZE_WIDTH ?
                        0 : SIZE_WIDTH - FileSizeLength;
         *vecptr++ = FileSizeLength;
         *vecptr++ = FileSizePtr;
     }

      *vecptr++ = FileDateLength;
      *vecptr++ = FileDatePtr;

      status = NetWriteFaol (rqptr, FileFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
   }

   vecptr = &FaoVector;

   if (rqptr->rqPathSet.HtmlFooterPtr ||
       rqptr->rqPathSet.HtmlFooterTagPtr)
   {
      if (rqptr->rqPathSet.HtmlFooterTagPtr &&
          rqptr->rqPathSet.HtmlFooterTagPtr[0] == '<')
         *vecptr++ = "!AZ\n!&@";
      else
         *vecptr++ =
"<TABLE CELLPADDING=5 CELLSPACING=0 BORDER=0 WIDTH=100%><TR><TD!&+AZ>\n!&@";
      *vecptr++ = rqptr->rqPathSet.HtmlFooterTagPtr;
      *vecptr++ = "!&/AZ</TD></TR></TABLE>\n";
      *vecptr++ = rqptr->rqPathSet.HtmlFooterPtr;
   }
   else
      *vecptr++ = "";
      
   status = NetWriteFaol (rqptr, EndPageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

#undef NAME_WIDTH
#undef SIZE_WIDTH
}

/*****************************************************************************/
/*
Provide the "Index of" page heading in any of the post-v8.2, traditional WASD,
or the "ABI" styles.
*/ 

int ProxyFtpIndexOf
(
PROXY_TASK *tkptr,
char *IndexOfPtr
)
{
   /* allows as directory nesting of up to 64 (should be enough ;^) */
   static char  DotDotSlash64 [] =
"<A HREF=\"./\
../../../../../../../../../../../../../../../../\
../../../../../../../../../../../../../../../../\
../../../../../../../../../../../../../../../../\
../../../../../../../../../../../../../../../../";

   int  cnt, status,
        SlashCount;
   unsigned long  FaoVector [32];
   unsigned long  *vecptr;
   char  *cptr, *sptr, *zptr,
         *FinalSlashPtr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpIndexOf() !&Z !AZ",
                 IndexOfPtr, tkptr->FtpFilePathPtr);

   rqptr = tkptr->RequestPtr;

   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_ORIGINAL)
   {   
      /**************************/
      /* traditional WASD style */
      /**************************/

      vecptr = &FaoVector;
      *vecptr++ = IndexOfPtr;
      *vecptr++ = tkptr->FtpFilePathPtr;
      status = NetWriteFaol (rqptr, "<H2><NOBR>!AZ &nbsp;!AZ</NOBR></H2>\n",
                             &FaoVector);
      return (status);
   }

   /****************/
   /* other styles */
   /****************/

   /* calculate the number of directory elements */
   SlashCount = 0;
   FinalSlashPtr = "";
   for (cptr = tkptr->FtpFilePathPtr; *cptr; cptr++)
   {
      if (*cptr == '/')
      {
         SlashCount++;
         FinalSlashPtr = cptr;
      }
   }
   if (SlashCount > 64) return (SS$_BUGCHECK);
   if (*FinalSlashPtr) FinalSlashPtr++;

   vecptr = &FaoVector;
   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
   {
      /* ABI's style begins with the server name as a 'root' anchor */
      if (rqptr->rqHeader.HostPtr &&
          rqptr->rqHeader.HostPtr[0])
         *vecptr++ = rqptr->rqHeader.HostPtr;
      else
         *vecptr++ = rqptr->ServicePtr->ServerHostPort;
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
      status = NetWriteFaol (rqptr, "<H3><NOBR><A HREF=\"/\">//!AZ!AZ/</A>",
                             &FaoVector);
   }
   else
   {
      /* WASD style provides an "Index of" heading */
      *vecptr++ = IndexOfPtr;
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
      if (rqptr->rqHeader.HostPtr &&
          rqptr->rqHeader.HostPtr[0])
         *vecptr++ = rqptr->rqHeader.HostPtr;
      else
         *vecptr++ = rqptr->ServicePtr->ServerHostPort;
      status = NetWriteFaol (rqptr,
                  "<H3><NOBR>!AZ &nbsp;//<A HREF=\"/!AZ\">!AZ</A>/",
                             &FaoVector);
   }
   if (VMSnok (status)) return (status);
   cptr = tkptr->FtpFilePathPtr;

   /* provide a self-relative (../) anchor for each directory element */
   if (SlashCount) SlashCount--;
   while (SlashCount-- > 0)
   {
      vecptr = &FaoVector;
      *vecptr++ = 11 + (SlashCount * 3);
      *vecptr++ = DotDotSlash64;
      if (SlashCount)
         *vecptr++ = FinalSlashPtr;
      else
         *vecptr++ = "";
      *vecptr++ = tkptr->RequestUriQueryStringPtr;
      if (*cptr == '/') cptr++;
      for (sptr = cptr; *sptr && *sptr != '/'; sptr++);
      if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
      {
         /* ABI's style, trailing slashes are part of the link */
         if (*sptr) sptr++;
         *vecptr++ = sptr - cptr;
         *vecptr++ = cptr;
         *vecptr++ = 0;
         *vecptr++ = "";
      }
      else
      {
         /* WASD style, trailing slashes are not part of the link */
         *vecptr++ = sptr - cptr;
         *vecptr++ = cptr;
         *vecptr++ = 1;
         *vecptr++ = sptr;
      }
      if (VMSnok (status)) return (status);
      status = NetWriteFaol (rqptr, "!#AZ!AZ!&;AZ\">!#AZ</A>!#AZ",
                             &FaoVector);
      cptr = sptr;
   }

   if (rqptr->rqPathSet.DirStyle == MAPURL_DIR_STYLE_HTDIR)
   {
      /* ABI's style, 'file' name and type element as an anchor */
      if (*cptr == '/') cptr++;
      vecptr = &FaoVector;
      *vecptr++ = cptr;
      status = NetWriteFaol (rqptr, "<A HREF=\"!AZ\">!-!AZ</A></NOBR></H3>\n",
                             &FaoVector);
   } 
   else
   {
      /* WASD style, 'file' name and type just displayed */
      if (*cptr == '/') cptr++;
      vecptr = &FaoVector;
      *vecptr++ = cptr;
      status = NetWriteFaol (rqptr, "!AZ</NOBR></H3>\n", &FaoVector);
   }

   return (status);
}

/****************************************************************************/
/*
Before retrieving a file set the FTP server transfer TYPE to ASCII or IMAGE
depending on whether carriage-control is require to be translated (at least in
some cases) or whether the file should be considered a bag-o'-bytes.
*/

ProxyFtpRetrieveMode (PROXY_TASK *tkptr)

{
   char  *cptr;
   CONTENT_TYPE  ContentType;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRetrieveMode()");

   rqptr = tkptr->RequestPtr;

   /* find the start of the file type (if any) */
   cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
   while (cptr > tkptr->FtpFilePathPtr && *cptr != '.' && *cptr != '/') cptr--;

   /* content type will subsequently be used by ProxyFtpRetrieve() */
   if (*cptr == '.')
      tkptr->FtpContentTypePtr = ConfigContentType (&ContentType, cptr);
   else
      tkptr->FtpContentTypePtr = ConfigDefaultFileContentType;

   /* is the client explicitly setting the transfer TYPE? */
   cptr = rqptr->rqHeader.QueryStringPtr;
   if (ContentType.FtpMode == 'A' ||
       ProxyFtpInQueryString (cptr, "ascii"))
      ProxyFtpCommand (tkptr, true, "TYPE A");
   else
   if (ContentType.FtpMode == 'I' ||
       ContentType.FtpMode == 'B' ||
       ProxyFtpInQueryString (cptr, "image"))
      ProxyFtpCommand (tkptr, true, "TYPE I");
   else
   if (strsame (tkptr->FtpContentTypePtr, "text/", 5))
      ProxyFtpCommand (tkptr, true, "TYPE A");
   else
      ProxyFtpCommand (tkptr, true, "TYPE I");
}

/****************************************************************************/
/*
Retrieve a file from the remote FTP server.  This function initializes the
required data and issues a RETR command to the FTP server.  If successful
ProxyFtpLifeCycle() will then call ProxyFtpRetrieveAst() to start retrieving
the file and writing it to the client.
*/

ProxyFtpRetrieve (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr, *sptr, *zptr;
   char  ContentType [64];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRetrieve()");

   rqptr = tkptr->RequestPtr;

   InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpRetrCount);

   cptr = rqptr->rqHeader.QueryStringPtr;
   if (cptr[0] && ProxyFtpInQueryString (cptr, "text"))
      ResponseHeader (rqptr, 200, "text/plain", -1, NULL, NULL);
   else
   if (cptr[0] && ProxyFtpInQueryString (cptr, "octet"))
      ResponseHeader (rqptr, 200, "application/octet-stream", -1, NULL, NULL);
   else
   if (cptr[0] && (sptr = ProxyFtpInQueryString (cptr, "content:")))
   {
      /* keyword style */
      cptr = sptr + 9;
      zptr = (sptr = ContentType) + sizeof(ContentType)-1;
      while (*cptr && *cptr != '+' && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
      ResponseHeader (rqptr, 200, ContentType, -1, NULL, NULL);
   }
   else
   if (cptr[0] && (sptr = ProxyFtpInQueryString (cptr, "content=")))
   {
      /* form field style */
      cptr = sptr + 9;
      zptr = (sptr = ContentType) + sizeof(ContentType)-1;
      while (*cptr && *cptr != '&' && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
      ResponseHeader (rqptr, 200, ContentType, -1, NULL, NULL);
   }
   else
      /* content type has been set up by ProxyFtpRetrieveMode() */
      ResponseHeader (rqptr, 200, tkptr->FtpContentTypePtr, -1, NULL, NULL);

   cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
   /* find the start of the file name */
   while (cptr > tkptr->FtpFilePathPtr && *cptr != '/') cptr--;
   cptr++;

   if (!tkptr->ResponseBufferPtr)
   {
      tkptr->ResponseBufferSize = ProxyReadBufferSize;
      tkptr->ResponseBufferPtr = VmGetHeap (rqptr, ProxyReadBufferSize);
   }

   /* reset the IO status block for the first read */
   tkptr->FtpDataReadIOsb.Status = tkptr->FtpDataReadIOsb.Count = 0;

   ProxyFtpCommand (tkptr, true, "RETR !AZ", cptr);
}

/****************************************************************************/
/*
When a read from the remote FTP server completes this function is called as an
AST.  Check the read status.  If OK then write it to the client, with an AST to 
ProxyFtpRetrieveWriteAst().  If not OK check if the status is reset.  If so
it's the first call and so kick off the loop with an initial read.  If a real
error then abort the transfer.
*/

ProxyFtpRetrieveAst (PROXY_TASK *tkptr)

{
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRetrieveAst() !&F !&S !UL", &ProxyFtpRetrieveAst,
                 tkptr->FtpDataReadIOsb.Status, tkptr->FtpDataReadIOsb.Count);

   rqptr = tkptr->RequestPtr;

   if (VMSok (tkptr->FtpDataReadIOsb.Status))
   {
      /* write it to the client */
      NetWrite (rqptr, &ProxyFtpRetrieveWriteAst,
                tkptr->ResponseBufferPtr,
                tkptr->FtpDataReadIOsb.Count);
      return;
   }

   if (!tkptr->FtpDataReadIOsb.Status)
   {
      /* first call for the retrieve, kick off the first read */
      ProxyFtpDataReadRaw (tkptr, &ProxyFtpRetrieveAst,
                           tkptr->ResponseBufferPtr,
                           tkptr->ResponseBufferSize);
      return;
   }

   if (tkptr->FtpDataReadIOsb.Status == SS$_LINKDISCON)
   {
      /* transfer is complete (or aborted!) */
      ProxyFtpDataCloseSocket (tkptr);
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, tkptr->FtpDataReadIOsb.Status, FI_LI);
   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
************
*** NOTE ***  This function takes a pointer to a request!!!
************

Called as an AST when a write to the client, queued by ProxyFtpRetrieveAst(),
completes.  Check the status of the write and if OK queue another read from the
FTP server.  If an error abort the transfer.
*/

ProxyFtpRetrieveWriteAst (REQUEST_STRUCT *rqptr)

{
   PROXY_TASK  *tkptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (rqptr, FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRetrieveWriteAst() !&S !UL",
                 rqptr->rqNet.WriteIOsb.Status,
                 rqptr->rqNet.WriteIOsb.Count);

   tkptr = rqptr->ProxyTaskPtr; 

   if (VMSok (rqptr->rqNet.WriteIOsb.Status))
   {
      ProxyFtpDataReadRaw (tkptr, &ProxyFtpRetrieveAst,
                           tkptr->ResponseBufferPtr,
                           tkptr->ResponseBufferSize);
      return;
   }

   /* write to client failed, just abandon the request */
   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Begin to read the request body.
*/

ProxyFtpStoreBodyReadBegin (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr, *sptr, *zptr;
   char  ContentType [64];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpStoreBodyReadBegin()");

   rqptr = tkptr->RequestPtr;

   InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpStorCount);

   /* initialize the data transfer data */
   if (ConfigSameContentType (rqptr->rqHeader.ContentTypePtr,
                              "application/x-www-form-urlencoded", -1))
      BodyReadBegin (rqptr, &ProxyFtpStoreBodyReadAst,
                     &BodyProcessUrlEncoded);
   else
   if (ConfigSameContentType (rqptr->rqHeader.ContentTypePtr,
                              "multipart/", 10))
      BodyReadBegin (rqptr, &ProxyFtpStoreBodyReadAst,
                     &BodyProcessMultipartFormData);
   else
      BodyReadBegin (rqptr, &ProxyFtpStoreBodyReadAst, NULL);
}

/****************************************************************************/
/*
A chunk of the request body has been read from the client.  This function will
be called at least twice.  After the first read, and the file name has been
resolved from the request body, it needs to issue the STOR command.  It does
this and after the response ProxyFtpLifeCycle() calls this same function (which
untouched body data) again.  Write the body data to the remote FTP server.
*/

ProxyFtpStoreBodyReadAst (REQUEST_STRUCT *rqptr)

{
   char  *cptr, *sptr;
   BODY_PROCESS  *prptr;
   PROXY_TASK  *tkptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (rqptr, FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpStoreBodyReadAst() !&F !&X !&S !&X !UL",
                 &ProxyFtpStoreBodyReadAst, rqptr->rqBody.ProcessPtr,
                 rqptr->rqBody.DataStatus, rqptr->rqBody.DataPtr,
                 rqptr->rqBody.DataCount);

   tkptr = rqptr->ProxyTaskPtr;
   prptr = rqptr->rqBody.ProcessPtr;

   if (!(tkptr->FtpTypeDone && tkptr->FtpTypeDone))
   {
      cptr = NULL;
      if (prptr)
      {
         if (prptr->MultipartFileName[0])
            cptr = prptr->MultipartFileName;
         if (prptr->MultipartUploadFileName[0])
             cptr = prptr->MultipartUploadFileName;
         if (sptr = cptr)
         {
            /* all we're interested is the name component */
            while (*cptr) cptr++;
            while (cptr > sptr && *cptr != '/' &&
                   *cptr != '\\' && *cptr != ']') cptr--;
            if (*cptr == '/' || *cptr == '\\' || *cptr == ']') cptr++;
         }
      }
      if (!cptr)
      {
         cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
         /* find the start of the file name */
         while (cptr > tkptr->FtpFilePathPtr && *cptr != '/') cptr--;
         cptr++;
      }
      if (!cptr || !*cptr)
      {
         rqptr->rqResponse.HttpStatus = 400;
         ErrorGeneral (rqptr, MsgFor(rqptr,MSG_PROXY_FTP_NO_FILENAME), FI_LI);
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         SysDclAst (&ProxyFtpLifeCycle, tkptr);
      }
   }

   if (!tkptr->FtpTypeDone)
   {
      /*********************/
      /* send TYPE command */
      /*********************/

      tkptr->FtpTypeDone = true;

      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         WatchThis (rqptr, FI_LI, WATCH_MOD_PROXY, "!&Z",
                    prptr ? prptr->FtpFileType : "");

      /* is the client explicitly setting the transfer TYPE? */
      cptr = rqptr->rqHeader.QueryStringPtr;
      if (ProxyFtpInQueryString (cptr, "ascii") ||
          (prptr && tolower(prptr->FtpFileType[0]) == 'a'))
         ProxyFtpCommand (tkptr, true, "TYPE A");
      else
      if (ProxyFtpInQueryString (cptr, "image") ||
          (prptr && tolower(prptr->FtpFileType[0]) == 'i'))
         ProxyFtpCommand (tkptr, true, "TYPE I");
      else
      if (prptr && prptr->MultipartContentType[0])
      {
         if (ConfigSameContentType (prptr->MultipartContentType, "text/", 5))
            ProxyFtpCommand (tkptr, true, "TYPE A");
         else
            ProxyFtpCommand (tkptr, true, "TYPE I");
      }
      else
      if (rqptr->rqHeader.ContentTypePtr)
      {
         if (ConfigSameContentType (rqptr->rqHeader.ContentTypePtr, "text/", 5))
            ProxyFtpCommand (tkptr, true, "TYPE A");
         else
            ProxyFtpCommand (tkptr, true, "TYPE I");
      }
      else
         ProxyFtpCommand (tkptr, true, "TYPE I");

      /* will return to this function with the STOR command done! */
      return;
   }

   if (!tkptr->FtpStorDone)
   {
      /*********************/
      /* send STOR command */
      /*********************/

      tkptr->FtpStorDone = true;

      cptr = NULL;
      if (prptr)
      {
         if (prptr->MultipartFileName[0])
            cptr = prptr->MultipartFileName;
         if (prptr->MultipartUploadFileName[0])
             cptr = prptr->MultipartUploadFileName;
         if (sptr = cptr)
         {
            /* all we're interested is the name component */
            while (*cptr) cptr++;
            while (cptr > sptr && *cptr != '/' &&
                   *cptr != '\\' && *cptr != ']') cptr--;
            if (*cptr == '/' || *cptr == '\\' || *cptr == ']') cptr++;
         }
      }
      if (!cptr)
      {
         cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
         /* find the start of the file name */
         while (cptr > tkptr->FtpFilePathPtr && *cptr != '/') cptr--;
         cptr++;
      }
      if (!cptr || !*cptr)
      {
         rqptr->rqResponse.HttpStatus = 400;
         ErrorGeneral (rqptr, MsgFor(rqptr,MSG_PROXY_FTP_NO_FILENAME), FI_LI);
         tkptr->FtpState = PROXY_FTP_STATE_QUIT;
         SysDclAst (&ProxyFtpLifeCycle, tkptr);
      }

      ProxyFtpCommand (tkptr, true, "STOR !AZ", cptr);

      /* will return to this function with the STOR command done! */
      return;
   }

   /*****************/
   /* transfer data */
   /*****************/

   if (VMSok (rqptr->rqBody.DataStatus))
   {
      /* write this buffered chunk of the request body to the FTP server */
      ProxyFtpDataWriteRaw (tkptr, &ProxyFtpStoreAst,
                            rqptr->rqBody.DataPtr, rqptr->rqBody.DataCount);
      return;
   }

   if (rqptr->rqBody.DataStatus == SS$_ENDOFFILE)
   {
      /* end of body */
      ProxyFtpDataCloseSocket (tkptr);
      SysDclAst (&ProxyFtpLifeCycle, tkptr);
      return;
   }

   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, rqptr->rqBody.DataStatus, FI_LI);
   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
A chunk of the request body has been written to the remote FTP server.
*/

ProxyFtpStoreAst (PROXY_TASK *tkptr)

{
   int  DataCount;
   char  *DataPtr;
   REQUEST_STRUCT *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpStoreAst() !&F !&S !UL", &ProxyFtpStoreAst,
                 tkptr->FtpDataWriteIOsb.Status, tkptr->FtpDataWriteIOsb.Count);

   rqptr = tkptr->RequestPtr;

   if (VMSok (tkptr->FtpDataWriteIOsb.Status))
   {
      BodyRead (rqptr);
      return;
   }

   rqptr->rqResponse.HttpStatus = 502;
   rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
   ErrorVmsStatus (rqptr, tkptr->FtpDataWriteIOsb.Status, FI_LI);
   tkptr->FtpState = PROXY_FTP_STATE_ABORT;
   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/****************************************************************************/
/*
Delete a file from the remote FTP server.  This function initializes the
required data and issues a DELE command to the FTP server.
*/

ProxyFtpDelete (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr, *sptr;
   char  ContentType [64];
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpRetrieve()");

   rqptr = tkptr->RequestPtr;

   InstanceGblSecIncrLong (&ProxyAccountingPtr->FtpDeleCount);

   if (tkptr->FtpFileSystem == PROXY_FTP_FILE_SYSTEM_VMS)
   {
      /* of course, VMS requires a version number! */
      cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
      while (cptr > tkptr->FtpFilePathPtr && *cptr != '/' && *cptr != ';')
         cptr--;
      if (cptr[0] == ';')
      {
         if (cptr[1])
            sptr = "";
         else
            sptr = "*";
      }
      else
      if (tkptr->FtpSpecific == PROXY_FTP_SPECIFIC_MADGOAT)
         sptr = "";
      else
         sptr = ";*";
   }
   else
      sptr = "";

   /* find the start of the file name */
   cptr = tkptr->FtpFilePathPtr + tkptr->FtpFilePathLength;
   while (cptr > tkptr->FtpFilePathPtr && *cptr != '/') cptr--;
   cptr++;

   if (!tkptr->ResponseBufferPtr)
   {
      tkptr->ResponseBufferSize = ProxyReadBufferSize;
      tkptr->ResponseBufferPtr = VmGetHeap (rqptr, ProxyReadBufferSize);
   }

   /* reset the IO status block for the first read */
   tkptr->FtpDataReadIOsb.Status = tkptr->FtpDataReadIOsb.Count = 0;

   ProxyFtpCommand (tkptr, true, "DELE !AZ!AZ", cptr, sptr);
}

/****************************************************************************/
/*
Searches the query string for the keyword.  Keywords are designed to be
detected whether supplied as "?keyword", "?keyword1+keyword2",
"keyword=anything", "keyword1=anything&keyword2=anything", or where the keyword
occurs as the value of a form field, for example "?type=ascii".  This allows
for simply adding the keyword to the URL or use in an HTML form.  Needless to
say, the keywords must be unique.  Returns a pointer to the start of the
keyword if found or NULL if not.
*/

char* ProxyFtpInQueryString
(
char *QueryStringPtr,
char *KeywordPtr
)
{
   int  KeywordLength;
   char  *cptr, *sptr;

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

   if (WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (NULL, FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpInQueryString() !&Z !&Z",
                 QueryStringPtr, KeywordPtr);

   KeywordLength = strlen(KeywordPtr);
   cptr = QueryStringPtr;
   while (*cptr)
   {
      /* "content=" is a special case for field value processing */
      if (strsame (cptr, "content=", 8) && strsame (KeywordPtr, "content", 7))
         return (cptr);
      /* all other the keywords can be anywhere in the string */
      if (strsame (cptr, KeywordPtr, KeywordLength)) return (cptr);
      while (*cptr && *cptr != '+' && !cptr != '&' && *cptr != '=') cptr++;
      if (*cptr) cptr++;
   }
   return (NULL);
}

/****************************************************************************/
/*
************
*** NOTE ***  This function takes a pointer to a request!!!
************

This function is called from ProxyRequestBegin() and so has nothing to do with
ProxyFtpLifeCycle().
*/

ProxyFtpStoreForm (REQUEST_STRUCT *rqptr)

{
   static char  FormFao [] =
"<HTML>\n\
<HEAD>\n\
<TITLE>FTP Upload</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<FORM ACTION=\"!AZ\" METHOD=POST ENCTYPE=\"multipart/form-data\">\n\
<INPUT TYPE=radio NAME=type VALUE=\"default\" CHECKED>default\n\
<INPUT TYPE=radio NAME=type VALUE=\"ascii\">ASCII\n\
<INPUT TYPE=radio NAME=type VALUE=\"image\">image\n\
<BR><INPUT TYPE=file NAME=name SIZE=25>\n\
<BR><INPUT TYPE=submit VALUE=\" !AZ \">\n\
</FORM>\n\
</BODY>\n\
</HTML>\n";

   char  *cptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (rqptr, FI_LI, WATCH_MOD_PROXY, "ProxyFtpStoreForm()");

   /* make sure this form is only called the once! */
   for (cptr = rqptr->rqHeader.RequestUriPtr; *cptr && *cptr != '?'; cptr++);
   if (*cptr) cptr++;
   cptr = ProxyFtpInQueryString (cptr, "upload");
   if (cptr) *cptr = 'v';

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_FTP_PROXY;
   ResponseHeader (rqptr, 200, "text/html", -1, NULL, NULL);
   NetWriteFao (rqptr, FormFao, rqptr->rqHeader.RequestUriPtr, "Upload");
}

/****************************************************************************/
/*
Attempt to open a socket connected to the IP address and port specified by the
PASV response.  This will become the data transfer connection.  ASTs to
ProxyFtpDataConnect().
*/

ProxyFtpDataConnect (PROXY_TASK *tkptr)

{
   static BOOL  UseFullDuplexClose = true;

   int  status;
   void  *BindSocketNamePtr;
   SOCKADDRIN  *sin4ptr;
   SOCKADDRIN6  *sin6ptr;
   TCP_SOCKET_ITEM  *TcpSocketPtr;
   VMS_ITEM_LIST2  *il2ptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataConnect()");

   if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                 "FTP-DATA connect !&I,!UL",
                 &tkptr->FtpDataIpAddress, tkptr->FtpDataIpPort);

   if (IPADDRESS_IS_SET (&tkptr->BindIpAddress))
   {
      /* bind the FTP data socket to a specific IP address */
      if (IPADDRESS_IS_V4 (&tkptr->BindIpAddress))
      {
         SOCKADDRESS_ZERO4 (&tkptr->FtpDataBindSocketName)
         sin4ptr = &tkptr->FtpDataBindSocketName.sa.v4;
         sin4ptr->SIN$W_FAMILY = TCPIP$C_AF_INET;
         sin4ptr->SIN$W_PORT = 0;
         IPADDRESS_SET4 (sin4ptr->SIN$L_ADDR, &tkptr->BindIpAddress)

         il2ptr = &tkptr->FtpDataBindSocketNameItem;
         il2ptr->buf_len = sizeof(SOCKADDRIN);
         il2ptr->item = 0;
         il2ptr->buf_addr = &tkptr->FtpDataBindSocketName.sa.v4;
      }
      else
      if (IPADDRESS_IS_V6 (&tkptr->BindIpAddress))
      {
         SOCKADDRESS_ZERO6 (&tkptr->FtpDataBindSocketName)
         sin6ptr = &tkptr->FtpDataBindSocketName.sa.v6;
         sin6ptr->SIN6$B_FAMILY = TCPIP$C_AF_INET;
         sin6ptr->SIN6$W_PORT = 0;
         IPADDRESS_SET6 (sin6ptr->SIN6$R_ADDR_OVERLAY.SIN6$T_ADDR,
                         &tkptr->BindIpAddress)

         il2ptr = &tkptr->FtpDataBindSocketNameItem;
         il2ptr->buf_len = sizeof(SOCKADDRIN6);
         il2ptr->item = 0;
         il2ptr->buf_addr = &tkptr->FtpDataBindSocketName.sa.v6;
      }
      else
         ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      BindSocketNamePtr = (void*)il2ptr;
   }
   else
      BindSocketNamePtr = 0;

   if (IPADDRESS_IS_V4 (&tkptr->FtpDataIpAddress))
      TcpSocketPtr = &TcpIpSocket4;
   else
   if (IPADDRESS_IS_V6 (&tkptr->FtpDataIpAddress))
      TcpSocketPtr = &TcpIpSocket6;
   else
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   for (;;)
   {
      /* assign a channel to the internet template device */
      status = sys$assign (&TcpIpDeviceDsc, &tkptr->FtpDataChannel, 0, 0);
      if (VMSnok (status))
      {
         /* leave it to the AST function to report! */
         tkptr->FtpDataConnectIOsb.Status = status;
         SysDclAst (ProxyFtpDataConnectAst, tkptr);
         return;
      }

      /* make the channel a TCP, connection-oriented socket */
      status = sys$qiow (EfnWait, tkptr->FtpDataChannel, IO$_SETMODE,
                         &tkptr->FtpDataConnectIOsb, 0, 0,
                         TcpSocketPtr, 0, BindSocketNamePtr,
                         0, UseFullDuplexClose ?
                            &TcpIpFullDuplexCloseOption : 0,
                         0);
      if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                       "sys$qiow() !&X !&X",
                       status, tkptr->FtpDataConnectIOsb.Status);

      if (VMSok (status) && VMSok (tkptr->FtpDataConnectIOsb.Status)) break;
      if (!UseFullDuplexClose) break;
      UseFullDuplexClose = false;

      /* Multinet 3.2 UCX driver barfs on FULL_DUPLEX_CLOSE, try without */
      sys$dassgn (tkptr->FtpDataChannel);
   }

   /* it's a $QIOW so the IO status block is valid */
   if (VMSok (status) && VMSnok (tkptr->FtpDataConnectIOsb.Status))
      status = tkptr->FtpDataConnectIOsb.Status;
   if (VMSnok (status))
   {
      /* leave it to the AST function to report! */
      tkptr->FtpDataConnectIOsb.Status = status;
      SysDclAst (ProxyFtpDataConnectAst, tkptr);
      return;
   }

   if (IPADDRESS_IS_V4 (&tkptr->FtpDataIpAddress))
   {
      SOCKADDRESS_ZERO4 (&tkptr->FtpDataSocketName)
      sin4ptr = &tkptr->FtpDataSocketName.sa.v4;
      sin4ptr->SIN$W_FAMILY = TCPIP$C_AF_INET;
      sin4ptr->SIN$W_PORT = tkptr->FtpDataIpPort;
      IPADDRESS_SET4 (sin4ptr->SIN$L_ADDR, &tkptr->FtpDataIpAddress)

      il2ptr = &tkptr->FtpDataSocketNameItem;
      il2ptr->buf_len = sizeof(SOCKADDRIN);
      il2ptr->item = TCPIP$C_SOCK_NAME;
      il2ptr->buf_addr = sin4ptr;
   }
   else
   if (IPADDRESS_IS_V6 (&tkptr->FtpDataIpAddress))
   {
      SOCKADDRESS_ZERO6 (&tkptr->FtpDataSocketName)
      sin6ptr = &tkptr->FtpDataSocketName.sa.v6;
      sin6ptr->SIN6$B_FAMILY = TCPIP$C_AF_INET6;
      sin6ptr->SIN6$W_PORT = tkptr->FtpDataIpPort;
      IPADDRESS_SET6 (sin6ptr->SIN6$R_ADDR_OVERLAY.SIN6$T_ADDR,
                      &tkptr->FtpDataIpAddress)

      il2ptr = &tkptr->FtpDataSocketNameItem;
      il2ptr->buf_len = sizeof(SOCKADDRIN6);
      il2ptr->item = TCPIP$C_SOCK_NAME;
      il2ptr->buf_addr = sin6ptr;
   }
   else
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   status = sys$qio (EfnNoWait, tkptr->FtpDataChannel, IO$_ACCESS,
                     &tkptr->FtpDataConnectIOsb, &ProxyFtpDataConnectAst, tkptr,
                     0, 0, &tkptr->FtpDataSocketNameItem, 0, 0, 0);
   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "sys$qiow() !&X", status);
   if (VMSnok (status))
   {
      /* leave it to the AST function to report! */
      tkptr->FtpDataConnectIOsb.Status = status;
      SysDclAst (ProxyFtpDataConnectAst, tkptr);
      return;
   }
}

/****************************************************************************/
/*
Called as an AST by ProxyFtpDataConnect() once the PASV data connection is
established or fails.
*/

ProxyFtpDataConnectAst (PROXY_TASK *tkptr)

{
   int  status;
   char  *cptr;
   REQUEST_STRUCT  *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataConnectAst() !&F !&S",
                 &ProxyFtpDataConnectAst, tkptr->FtpDataConnectIOsb.Status);

   rqptr = tkptr->RequestPtr;

   if (VMSnok (tkptr->FtpDataConnectIOsb.Status))
   {
      /*****************/
      /* connect error */
      /*****************/

      if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
         WatchThis (rqptr, FI_LI, WATCH_PROXY, "FTP-DATA connect !&S !-%!&M",
                    tkptr->FtpDataConnectIOsb.Status);

      ProxyFtpDataCloseSocket (tkptr);

      tkptr->ResponseStatusCode = 502;
      rqptr->rqResponse.HttpStatus = 502;
      switch (tkptr->FtpDataConnectIOsb.Status)
      {
         case PROXY_ERROR_CONNECT_REFUSED :
            cptr = MsgFor(rqptr,MSG_PROXY_CONNECT_REFUSED);
            ErrorGeneral (rqptr, cptr, FI_LI);
            break;
         case PROXY_ERROR_HOST_UNREACHABLE :
            cptr = MsgFor(rqptr,MSG_PROXY_HOST_UNREACHABLE);
            ErrorGeneral (rqptr, cptr, FI_LI);
            break;
         default :
            rqptr->rqResponse.ErrorTextPtr = tkptr->RequestHostPort;
            ErrorVmsStatus (rqptr, tkptr->FtpDataConnectIOsb.Status, FI_LI);
      }
   }

   SysDclAst (&ProxyFtpLifeCycle, tkptr);
}

/*****************************************************************************/
/*
Write to the data connection of the FTP server. Explicitly declares any AST
routine if an error occurs. The calling function must not do any error recovery
if an AST routine has been supplied but the associated AST routine must! If an
AST was not supplied then the return status can be checked.  AST calls
ProxyFtpWriteRawAST() which will then call the supplied AST function.
*/

int ProxyFtpDataWriteRaw
(
PROXY_TASK *tkptr,
PROXY_AST AstFunction,
char *DataPtr,
int DataCount
)

{
   int  status;
   REQUEST_STRUCT *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataWriteRaw() !&F !&A !&X !UL",
                 &ProxyFtpDataWriteRaw, AstFunction, DataPtr, DataCount);

   /* FTP data should never have blocking IO queued against it */
   if (!AstFunction) ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   rqptr = tkptr->RequestPtr;

   if (tkptr->FtpDataWriteRawAstFunction || !DataCount)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
   tkptr->FtpDataWriteRawAstFunction = AstFunction;
   tkptr->FtpDataWriteRawDataPtr = DataPtr;
   tkptr->FtpDataWriteRawDataCount = DataCount;

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_NETWORK_OCTETS))
   {
      WatchThis (rqptr, FI_LI, WATCH_NETWORK,
                 "WRITE !UL bytes", DataCount);
      WatchDataDump (DataPtr, DataCount);
   }

   status = sys$qio (EfnNoWait, tkptr->FtpDataChannel,
                     IO$_WRITEVBLK, &tkptr->FtpDataWriteIOsb,
                     &ProxyFtpDataWriteRawAst, tkptr,
                     DataPtr, DataCount, 0, 0, 0, 0);

   if (VMSok (status)) return (status);

   /* if resource wait enabled the only quota not waited for is ASTLM */
   if (status == SS$_EXQUOTA)
      ErrorExitVmsStatus (status, "sys$qio()", FI_LI);

   /* write failed, call AST explicitly, status in the IOsb */
   tkptr->FtpDataWriteIOsb.Status = status;
   tkptr->FtpDataWriteIOsb.Count = 0;
   SysDclAst (ProxyFtpDataWriteRawAst, tkptr);
   return (status);
}

/*****************************************************************************/
/*
AST from ProxyFtpDataWriteRaw().  Call the AST function.
*/ 

ProxyFtpDataWriteRawAst (PROXY_TASK *tkptr)

{
   int  status;
   PROXY_AST  AstFunction;
   REQUEST_STRUCT *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataWriteRawAst() !&F !&S !UL",
                 &ProxyFtpDataWriteRawAst,
                 tkptr->FtpDataWriteIOsb.Status, tkptr->FtpDataWriteIOsb.Count);

   rqptr = tkptr->RequestPtr;

   if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_NETWORK))
   {
      if (VMSnok(tkptr->FtpDataWriteIOsb.Status))
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_NETWORK,
                    "WRITE !&S !-%!&M",
                    tkptr->FtpDataWriteIOsb.Status);
      else
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_NETWORK,
                    "WRITE !&S !UL bytes",
                    tkptr->FtpDataWriteIOsb.Status,
                    tkptr->FtpDataWriteIOsb.Count);
   }

   if (VMSok (tkptr->FtpDataWriteIOsb.Status))
      tkptr->BytesRawTx += tkptr->FtpDataWriteIOsb.Count;

   tkptr->FtpDataWriteRawDataPtr = tkptr->FtpDataWriteRawDataCount = 0;
   if (!tkptr->FtpDataWriteRawAstFunction) return;
   AstFunction = tkptr->FtpDataWriteRawAstFunction;
   tkptr->FtpDataWriteRawAstFunction = NULL;
   (*AstFunction)(tkptr);
}

/*****************************************************************************/
/*
Queue up a read from the data port of the FTP server. If 'AstFunction' is zero
then no I/O completion AST routine is called.  If it is non-zero then the
function pointed to by the parameter is called when the network write 
completes.

Explicitly declares any AST routine if an error occurs. The calling function
must not do any error recovery if an AST routine has been supplied but the
associated AST routine must!  If an AST was not supplied then the return
status can be checked.
*/ 

int ProxyFtpDataReadRaw
(
PROXY_TASK *tkptr,
PROXY_AST AstFunction,
char *DataPtr,
int DataSize
)
{
   int  status;
   REQUEST_STRUCT *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataReadRaw() !&F !&A !&X !UL",
                 &ProxyFtpDataReadRaw, AstFunction, DataPtr, DataSize);

   /* FTP data should never have blocking IO queued against it */
   if (!AstFunction) ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   rqptr = tkptr->RequestPtr;

   if (tkptr->FtpDataReadRawAstFunction)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
   tkptr->FtpDataReadRawAstFunction = AstFunction;
   tkptr->FtpDataReadRawDataPtr = DataPtr;
   tkptr->FtpDataReadRawDataSize = DataSize;

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_NETWORK_OCTETS))
      WatchThis (rqptr, FI_LI, WATCH_NETWORK,
                 "READ !UL bytes max", DataSize);

   status = sys$qio (EfnNoWait, tkptr->FtpDataChannel,
                     IO$_READVBLK, &tkptr->FtpDataReadIOsb,
                     &ProxyFtpDataReadRawAst, tkptr,
                     DataPtr, DataSize, 0, 0, 0, 0);

   if (VMSok (status)) return (status);

   /* with resource wait enabled the only quota not waited for is ASTLM */
   if (status == SS$_EXQUOTA)
      ErrorExitVmsStatus (status, "sys$qio()", FI_LI);

   /* queuing of read failed, call AST explicitly, status in the IOsb */
   tkptr->FtpDataReadIOsb.Status = status;
   tkptr->FtpDataReadIOsb.Count = 0;
   SysDclAst (ProxyFtpDataReadRawAst, tkptr);
   return (status);
}

/*****************************************************************************/
/*
AST from ProxyFtpDataReadRaw().  Call the AST function.
*/ 

ProxyFtpDataReadRawAst (PROXY_TASK *tkptr)

{
   int  status;
   PROXY_AST  AstFunction;
   REQUEST_STRUCT *rqptr;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataReadRawAst() !&F !&S !UL",
                 &ProxyFtpDataReadRawAst,
                 tkptr->FtpDataReadIOsb.Status, tkptr->FtpDataReadIOsb.Count);

   rqptr = tkptr->RequestPtr;

   if (WATCHING(tkptr))
   {
      if (WATCH_CATEGORY(WATCH_NETWORK) ||
          WATCH_CATEGORY(WATCH_NETWORK_OCTETS))
      {
         if (VMSok(tkptr->FtpDataReadIOsb.Status))
         {
            WatchThis (WATCHTK(tkptr), FI_LI, WATCH_NETWORK,
                       "READ !&S !UL bytes",
                       tkptr->FtpDataReadIOsb.Status,
                       tkptr->FtpDataReadIOsb.Count);

            if WATCH_CATEGORY(WATCH_NETWORK_OCTETS)
               WatchDataDump (tkptr->FtpDataReadRawDataPtr,
                              tkptr->FtpDataReadIOsb.Count);
         }
         else
            WatchThis (WATCHTK(tkptr), FI_LI, WATCH_NETWORK,
                       "READ !&S !-%!&M",
                       tkptr->FtpDataReadIOsb.Status);
      }
   }

   if (VMSok (tkptr->FtpDataReadIOsb.Status))
   {
      tkptr->BytesRawRx += tkptr->FtpDataReadIOsb.Count;
      /* zero bytes with a normal status is a definite no-no (TGV-Multinet) */
      if (!tkptr->FtpDataReadIOsb.Count)
         tkptr->FtpDataReadIOsb.Status = SS$_ABORT;
   }

   tkptr->FtpDataReadRawDataPtr = tkptr->FtpDataReadRawDataSize = 0;
   if (!tkptr->FtpDataReadRawAstFunction) return;
   AstFunction = tkptr->FtpDataReadRawAstFunction;
   tkptr->FtpDataReadRawAstFunction = NULL;
   (*AstFunction)(tkptr);
}

/****************************************************************************/
/*
Just shut the FTP data socket down, bang!
*/

int ProxyFtpDataCloseSocket (PROXY_TASK *tkptr)

{
   int  status;

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpDataCloseSocket() !&F", &ProxyFtpDataCloseSocket);

   if (!tkptr->FtpDataChannel) return (SS$_NORMAL);

   status = sys$dassgn (tkptr->FtpDataChannel);
   if (Debug) fprintf (stdout, "sys$dassgn() %%X%08.08X\n", status);
   tkptr->FtpDataChannel = 0;

   if (WATCHING(tkptr) && WATCH_CATEGORY(WATCH_PROXY))
   {
      if (VMSok(status))
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                    "FTP-DATA close !AZ,!UL",
                    tkptr->RequestHostName, tkptr->RequestPort); 
      else
         WatchThis (WATCHTK(tkptr), FI_LI, WATCH_PROXY,
                    "FTP-DATA close !AZ,!UL !&S !-%!&M",
                    tkptr->RequestHostName, tkptr->RequestPort, status);
   }

   return (status);
}

/****************************************************************************/
/*
Map FTP response status codes over into HTTP response status codes.
*/

int ProxyFtpHttpStatus (PROXY_TASK *tkptr)

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

   if (WATCHING(tkptr) && WATCH_MODULE(WATCH_MOD_PROXY))
      WatchThis (WATCHTK(tkptr), FI_LI, WATCH_MOD_PROXY,
                 "ProxyFtpHttpStatus() !UL", tkptr->FtpResponseCode);

   switch (tkptr->FtpResponseCode)
   {
      case 202 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 501);
      case 257 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 201);
      case 421 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 503);
      case 425 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 426 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 450 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 404);
      case 451 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 452 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 500 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 501 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 502 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 501);
      case 503 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 504 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 501);
      case 530 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 401);
      case 532 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 403);
      case 550 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 404);
      case 551 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 502);
      case 552 : return (tkptr->RequestPtr->rqResponse.HttpStatus = 403);
   }
   return (tkptr->RequestPtr->rqResponse.HttpStatus = 500);
}

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

