/*****************************************************************************/
/*
                                ProxySocks.c

RFC 1928 SOCKS Version 5 proxy for TCP/IP.

https://tools.ietf.org/html/rfc1928

This module supports only CONNECT TCP/IP and not BIND or UDP-associate.

This is not a "SOCKS5 compliant" implementation since the RFC requires that
compliant implementation MUST support GSSAPI, which this does not.

Username/Password Authentication for SOCKS V5

https://tools.ietf.org/html/rfc1929


VERSION HISTORY
---------------
15-MAR-2021  MGD  initial
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

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

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

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

#define WASD_MODULE "PROXYSOCKS"

static char  *Socks5Atyp  [] = { "*error*", "IPv4", "*error*",
                                 "domain", "IPv6" };
static char  *Socks5Cmd   [] = { "*error*", "connect", "bind", "udp"};
static char  *Socks5Reply [] = { "success", "failure", "ruleset",
                                 "netunreach", "hostunreach", "refused",
                                 "TTLexp", "command", "address" };

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

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

extern char SoftwareID[];

extern PROXY_ACCOUNTING_STRUCT  *ProxyAccountingPtr;
extern WATCH_STRUCT  Watch;

/****************************************************************************/
/*
Support SOCKS 5 proxy.  RFC 1928 Section 3.
*/

void ProxySocks5 (REQUEST_STRUCT *rqptr)

{
   static char  ReplyAuthNotOk [] = { SOCKS5_VERSION, SOCKS5_AUTH_NOTOK };
   static char  ReplyAuthOk [] = { SOCKS5_VERSION, 0 };

   char  cnt, idx;
   char  *bptr;

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

   if (WATCHING (rqptr, WATCH_PROXY))
       WatchThis (WATCHITM(rqptr), WATCH_PROXY, "SOCKS5 authenticate !UL",
                  rqptr->rqNet.ReadBufferPtr[1]);

   InstanceGblSecIncrLong (&ProxyAccountingPtr->Socks5Count);

   cnt = rqptr->rqNet.ReadBufferPtr[1];
   bptr = rqptr->rqNet.ReadBufferPtr+2;
   for (idx = 0; idx < cnt; idx++)
   {
      if (rqptr->ServicePtr->ProxyAuthRequired) {
         if (bptr[idx] == SOCKS5_AUTH_USERPASS)
            break;
      }
      else
      if (bptr[idx] == SOCKS5_AUTH_NONE)
         break;
   }
   if (idx >= cnt)
   {
      /* no supported authentication method */
      NetWrite (rqptr, &RequestEnd5, ReplyAuthNotOk, sizeof(ReplyAuthNotOk));
      return;
   }
   ReplyAuthOk[1] = bptr[idx];

   /* reply to the client that authorisation method is supported */
   if (bptr[idx] == SOCKS5_AUTH_USERPASS)
   {
      if (WATCHING (rqptr, WATCH_PROXY))
         WatchThis (WATCHITM(rqptr), WATCH_PROXY,
                    "SOCKS5 authenticate USERPASS");
      NetWrite (rqptr, 0, ReplyAuthOk, sizeof(ReplyAuthOk));
      NetRead (rqptr, &ProxySocks5BasicAst, rqptr->rqNet.ReadBufferPtr,
                                            rqptr->rqNet.ReadBufferSize);
   }
   else
      NetWrite (rqptr, &ProxySocks5GetRequest,
                ReplyAuthOk, sizeof(ReplyAuthOk));
}

/****************************************************************************/
/*
Process the SOCKS 5 basic authentication from the client.  RFC 1929 section 2.

           +----+------+----------+------+----------+
           |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
           +----+------+----------+------+----------+
           | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
           +----+------+----------+------+----------+

WASD implements this in a slightly "cunning" fashion (to accomodate the way
WASD proxy authentication operates in general).  When the SOCKS client provides
the username and password this is consolidated as a BASIC authentication
base-64 encoded string *temporarily* in rqptr->RemoteUser +
rqptr->RemoteUserPassword storage (not intended purpose), then incorporated
into a Proxy-Authorization: header field when the request is constructed by
ProxySocks5RequestAst().

So, WASD SOCK5 proxy authentication always returns to the client an initial
authentication OK reply and accepts the client's request, from which it builds
the proxy request which includes a Proxy-Authorization: header field with the
previously supplied BASIC authentication.  The proxy request is then processed
and if not authorised *then* returns a failure reply.  Not optimal but it
works.  The reason for the failure (not authorised) can be WATCHed for.
*/

void ProxySocks5BasicAst (REQUEST_STRUCT *rqptr)

{
   static char  ReplyAuthNotOk [] = { SOCKS5_VERSION, SOCKS5_AUTH_NOTOK };
   static char  ReplyAuthOk [] = { SOCKS5_VERSION, SOCKS5_AUTH_OK };

   int  cnt, idx, plen, ulen, b64len;
   char  *aptr, *bptr, *bzptr, *sptr, *zptr;
   char  UserPass [256];

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

   if (VMSnok (rqptr->NetIoPtr->ReadStatus))
   {
      /* read from client failed */
      RequestEnd5 (rqptr);
      return;
   }

   cnt = rqptr->NetIoPtr->ReadCount;
   bptr = rqptr->rqNet.ReadBufferPtr;
   bzptr = bptr + cnt;

   if (*bptr++ != SOCKS5_AUTH_BASIC_VERSION)
   {
      /* questionable data */
      ProxySocks5ReplyFail (rqptr);
      RequestEnd5 (rqptr);
      return;
   }

   /* second this storage that will not be used 'til after authorization */
   zptr = (sptr = UserPass) + sizeof(UserPass)-1;
   for (cnt = *bptr++; cnt-- && bptr < bzptr && sptr < zptr; *sptr++ = *bptr++);
   if (cnt >= 0)
   {
      /* questionable data */
      ProxySocks5ReplyFail (rqptr);
      RequestEnd5 (rqptr);
      return;
   }
   ulen = sptr - UserPass;
   if (sptr < zptr) *sptr++ = ':';
   aptr = sptr;
   for (cnt = *bptr++; cnt-- && bptr < bzptr && sptr < zptr; *sptr++ = *bptr++);
   if (cnt >= 0)
   {
      /* questionable data */
      ProxySocks5ReplyFail (rqptr);
      RequestEnd5 (rqptr);
      return;
   }
   plen = sptr - aptr;
   *sptr = '\0';

   if (WATCHING (rqptr, WATCH_PROXY))
       WatchThis (WATCHITM(rqptr), WATCH_PROXY, "SOCKS5 basic !#AZ:!#**",
                  ulen, UserPass, plen);

   b64len = sizeof(rqptr->RemoteUser) + sizeof(rqptr->RemoteUserPassword);
   base64_encode (rqptr->RemoteUser, &b64len, UserPass, ulen+1+plen);

   NetWrite (rqptr, &ProxySocks5GetRequest, ReplyAuthOk, sizeof(ReplyAuthOk));
}

/****************************************************************************/
/*
Read the SOCKS 5 request from the client.  RFC 1928 Section 4.
*/

void ProxySocks5GetRequest (REQUEST_STRUCT *rqptr)

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

   if (VMSnok (rqptr->NetIoPtr->WriteStatus))
   {
      /* write to client failed */
      RequestEnd5 (rqptr);
      return;
   }

   /* read request from client */
   NetRead (rqptr, &ProxySocks5RequestAst, rqptr->rqNet.ReadBufferPtr,
                                           rqptr->rqNet.ReadBufferSize);
}

/****************************************************************************/
/*
Client has provided a command.   RFC 1928 Section 4.
Process that and if accepted generated a "faked" WASD proxy CONNECT request.

        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

        o  VER    protocol version: X'05'
        o  CMD
           o  CONNECT X'01'
           o  BIND X'02'
           o  UDP ASSOCIATE X'03'
        o  RSV    RESERVED
        o  ATYP   address type of following address
           o  IP V4 address: X'01'
           o  DOMAINNAME: X'03'
           o  IP V6 address: X'04'
        o  DST.ADDR       desired destination address
        o  DST.PORT desired destination port in network octet order
*/

void ProxySocks5RequestAst (REQUEST_STRUCT *rqptr)

{
#define STRCAT(string) { \
   for (cptr = string; *cptr && sptr < zptr; *sptr++ = *cptr++); \
}
   int  atyp, cmd, cnt, idx, len;
   ulong  HostOrder [4];
   ushort  port;
   char  *aptr, *bptr, *cptr, *sptr, *zptr;
   char  dname [128],
         sport [8];

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

   if (VMSnok (rqptr->NetIoPtr->ReadStatus))
   {
      /* read from client failed */
      RequestEnd5 (rqptr);
      return;
   }

   cnt = rqptr->NetIoPtr->ReadCount;
   bptr = rqptr->rqNet.ReadBufferPtr;

   if (cnt < 10 || bptr[0] != SOCKS5_VERSION)
   {
      /* questionable data */
      ProxySocks5ReplyFail (rqptr);
      RequestEnd5 (rqptr);
      return;
   }

   cmd = bptr[1];
   if (cmd < 1 || cmd > 3) cmd = 0;

   atyp = bptr[3];
   if (atyp == SOCKS5_ATYPE_IPV4)
   {
      HostOrder[0] = *(ULONGPTR)(bptr+4);
      port = ntohs(*(USHORTPTR)(bptr+8));
      sprintf (sport, ":%d", port);
      aptr = TcpIpAddressToString(HostOrder[0],4);
      if (cnt != 10) len = 0;
   }
   else
   if (atyp == SOCKS5_ATYPE_DOMAIN)
   {
      len = bptr[4];
      aptr = dname;
      zptr = (sptr = dname) + sizeof(dname)-1;
      for (cptr = bptr + 5; len && sptr < zptr; len--) *sptr++ = *cptr++;
      *sptr = '\0';
      port = ntohs(*(USHORTPTR)cptr);
      sprintf (sport, ":%d", port);
      len = bptr[4] + 7;
      if (len != cnt) len = 0;
   }
   else
   if (atyp == SOCKS5_ATYPE_IPV6)
   {
      HostOrder[0] = *(ULONGPTR)(bptr+4);
      HostOrder[1] = *(ULONGPTR)(bptr+8);
      HostOrder[2] = *(ULONGPTR)(bptr+12);
      HostOrder[3] = *(ULONGPTR)(bptr+16);
      port = ntohs(*(USHORTPTR)(bptr+20));
      sprintf (sport, ":%d", port);
      aptr = TcpIpAddressToString(HostOrder,6);
      /* convert ':' to '-' delimited so not to confuse port */
      for (cptr = aptr; *cptr; cptr++) if (*cptr == ':') *cptr = '-';
      if (cnt != 22) len = 0;
   }
   else
      aptr = len = port = 0;

   if (WATCHING (rqptr, WATCH_PROXY))
      WatchThis (WATCHITM(rqptr), WATCH_PROXY,
                 "SOCKS5 !AZ !AZ !AZ port !UL",
                 Socks5Cmd[cmd], Socks5Atyp[atyp], aptr, port);
 
   /* only SOCKS5 connect requests are supported */
   if (!len || cmd != SOCKS5_CMD_CONNECT)
   {
      ProxySocks5ReplyFail (rqptr);
      RequestEnd5 (rqptr);
      return;
   }

   /* fake a CONNECT request */
   zptr = (sptr = rqptr->rqNet.ReadBufferPtr) + rqptr->rqNet.ReadBufferSize;
   STRCAT ("CONNECT socks5://");
   STRCAT (aptr);
   STRCAT (sport);
   STRCAT (" HTTP/1.1\r\nUser-Agent: ");
   STRCAT (SoftwareID);
   STRCAT ("\r\nHost: ");
   STRCAT (aptr);
   STRCAT (sport);
   if (rqptr->RemoteUser[0])
   {
      STRCAT ("\r\nProxy-Authorization: Basic ");
      STRCAT (rqptr->RemoteUser);
      rqptr->RemoteUser[0] = rqptr->RemoteUserPassword[0] = '\0';
   }
   STRCAT ("\r\nX-Forwarded-For: ");
   STRCAT (rqptr->ClientPtr->IpAddressString);
   STRCAT ("\r\nUpgrade: WASD-SOCKS5-");
   STRCAT (Socks5Cmd[cmd]);
   STRCAT ("-");
   STRCAT (Socks5Atyp[atyp]);
   STRCAT ("\r\n\r\n");
   /* as if it came from the network */
   rqptr->NetIoPtr->ReadCount = sptr - rqptr->rqNet.ReadBufferPtr;

   /* after this call the request will have a proxy task */
   SysDclAst (RequestGet, rqptr);

#undef STRCAT
}

/****************************************************************************/
/*
Provide the client with succeeded/failed reply.   RFC 1928 Section 6.
All replies to the client are blocking and synchronous.

        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

     Where:

          o  VER    protocol version: X'05'
          o  REP    Reply field:
             o  X'00' succeeded
             o  X'01' general SOCKS server failure
             o  X'02' connection not allowed by ruleset
             o  X'03' Network unreachable
             o  X'04' Host unreachable
             o  X'05' Connection refused
             o  X'06' TTL expired
             o  X'07' Command not supported
             o  X'08' Address type not supported
             o  X'09' to X'FF' unassigned
          o  RSV    RESERVED
          o  ATYP   address type of following address
*/

void ProxySocks5Reply
(
REQUEST_STRUCT *rqptr,
int ReplyField
)
{
   int  atyp, b_len, port, reply;
   uint  blen;
   ulong  Ip4Address;
   uchar Ip6Address;
   char  *aptr, *bptr;
   PROXY_TASK  *tkptr;

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

   tkptr = rqptr->ProxyTaskPtr; 

   if (ReplyField < 0 || ReplyField > SOCKS5_REPLY_ADDRESS)
   {
      ProxySocks5ReplyFail (rqptr);
      ProxySocks5End (rqptr);
      return;
   }

   bptr = tkptr->Socks5Reply;
   bptr[0] = SOCKS5_VERSION;
   bptr[1] = reply = ReplyField;
   bptr[2] = SOCKS5_RESERVED;
   if (IPADDRESS_IS_V4 (&tkptr->ConnectIpAddress))
   {
      bptr[3] = atyp = SOCKS5_ATYPE_IPV4;
      bptr += 4;
      IPADDRESS_SET4 (&Ip4Address, &tkptr->ConnectIpAddress)
      *(ULONGPTR)bptr = Ip4Address;
      aptr = TcpIpAddressToString(bptr,4);
      bptr += sizeof(Ip4Address);
   }
   else
   if (IPADDRESS_IS_V6 (&tkptr->ConnectIpAddress))
   {
      bptr[3] = atyp = SOCKS5_ATYPE_IPV6;
      bptr += 4;
      memset (&Ip6Address, 0, sizeof(Ip6Address));
      IPADDRESS_SET6 (Ip6Address, &tkptr->ConnectIpAddress)
      memcpy (bptr, &Ip6Address, sizeof(Ip6Address));
      aptr = TcpIpAddressToString(bptr,6);
      bptr += sizeof(Ip6Address);
   }
   else
   {
      ProxySocks5ReplyFail (rqptr);
      ProxySocks5End (rqptr);
      return;
   }
   *(USHORTPTR)bptr = port = (ushort)tkptr->ConnectPort;
   bptr += 2;

   if (!ReplyField)
      InstanceGblSecIncrLong (&ProxyAccountingPtr->Socks5SuccessCount);
   else
      InstanceGblSecIncrLong (&ProxyAccountingPtr->Socks5FailCount);

   blen = bptr - tkptr->Socks5Reply;
   bptr = tkptr->Socks5Reply;

   if (WATCHING (rqptr, WATCH_PROXY))
      WatchThis (WATCHITM(rqptr), WATCH_PROXY,
                 "SOCKS5 !AZ !AZ !AZ port !UL",
                 Socks5Reply[reply], Socks5Atyp[atyp], aptr, port);
 
   /* blocking with no AST delivery */
   NetWrite (rqptr, NULL, bptr, blen);
}

/****************************************************************************/
/*
All replies to the client are blocking and synchronous.
*/

void ProxySocks5ReplyFail (REQUEST_STRUCT *rqptr)

{
   static char  ReplyFail [] = { SOCKS5_VERSION, SOCKS5_REPLY_FAILURE,
                                 SOCKS5_RESERVED, SOCKS5_ATYPE_IPV4,
                                 0, 0, 0, 0, 0, 0 };
   /*********/
   /* begin */
   /*********/

   if (WATCHING (rqptr, WATCH_PROXY))
       WatchThis (WATCHITM(rqptr), WATCH_PROXY, "SOCKS5 reply FAIL");

   InstanceGblSecIncrLong (&ProxyAccountingPtr->Socks5FailCount);

   NetWrite (rqptr, 0, ReplyFail, sizeof(ReplyFail));
}

/****************************************************************************/
/*
Call proxy end from an AST delivering a request pointer.
*/

void ProxySocks5End (REQUEST_STRUCT *rqptr)

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

   ProxyEnd (rqptr->ProxyTaskPtr);
}

/****************************************************************************/
/*
Don't support SOCKS 4 proxy.
*/

void ProxySocks4 (REQUEST_STRUCT *rqptr)

{
   static char  ReplyNotSup [] = { 4, 91, 0, 0, 0, 0, 0, 0, 0 };

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

   if (WATCHING (rqptr, WATCH_PROXY))
       WatchThis (WATCHITM(rqptr), WATCH_PROXY, "SOCKS4 unsupported");

   NetWrite (rqptr, &RequestEnd5, ReplyNotSup, sizeof(ReplyNotSup));
}

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

