/* source: xio-socks5.c */
/* Copyright Gerhard Rieger and contributors (see file CHANGES) */
/* Published under the GNU General Public License V.2, see file COPYING */

/* this file contains the source for opening addresses of socks5 type */

#include "xiosysincludes.h"
#include "xioopen.h"
#include "xio-socket.h"
#include "xio-ipapp.h"

#include "xio-socks5.h"


#if WITH_SOCKS5

#define SOCKSPORT "1080"
#define SOCKS5_MAXLEN 512

static int 
   xioopen_socks5_client(int argc, const char *argv[],
			  struct opt *opts, int xioflags, xiofile_t *xfd,
			  unsigned groups, int dummy1, int dummy2,
			  int dummy3);

const struct optdesc opt_socks5_username  = { "socks5-username",  "socks5user", OPT_SOCKS5_USERNAME,  GROUP_SOCKS5, PH_LATE, TYPE_STRING,  OFUNC_SPEC };
const struct optdesc opt_socks5_password  = { "socks5-password",  "socks5pass", OPT_SOCKS5_PASSWORD,  GROUP_SOCKS5, PH_LATE, TYPE_STRING,  OFUNC_SPEC };

static const struct xioaddr_inter_desc xiointer_socks5_client = { XIOADDR_PROT, "socks5", 2, XIOBIT_ALL, GROUP_SOCKS5, XIOSHUT_DOWN, XIOCLOSE_CLOSE, xioopen_socks5_client, 0, 0, 0, XIOBIT_RDWR HELP(":<host>:<port>") };
const union xioaddr_desc *xioaddrs_socks5_client[] = {
   (union xioaddr_desc *)&xiointer_socks5_client, NULL };

/* read until buflen bytes received or EOF */
/* returns STAT_OK, STAT_RETRYLATER, or STAT_NOTRETRY */
static int xiosocks5_recvbytes(struct single *xfd,
			       uint8_t *buff, size_t buflen, int level) {
   size_t bytes;
   ssize_t result;

   bytes = 0;
   while (bytes >= 0) {	/* loop over answer chunks until complete or error */
      /* receive socks answer */
      Debug("waiting for data from peer");
      do {
	 result = Read(xfd->rfd, buff+bytes, buflen-bytes);
      } while (result < 0 && errno == EINTR);
      if (result < 0) {
	 Msg4(level, "read(%d, %p, "F_Zu"): %s",
	      xfd->rfd, buff+bytes, buflen-bytes,
	      strerror(errno));
	 if (Close(xfd->rfd) < 0) {
	    Warn2("close(%d): %s", xfd->rfd, strerror(errno));
	 }
	 if (Close(xfd->wfd) < 0) {
	    Warn2("close(%d): %s", xfd->wfd, strerror(errno));
	 }
	 return STAT_RETRYLATER;
      }
      if (result == 0) {
	 Msg(level, "read(): EOF during read of socks reply");
	 if (Close(xfd->rfd) < 0) {
	    Warn2("close(%d): %s", xfd->rfd, strerror(errno));
	 }
	 if (Close(xfd->wfd) < 0) {
	    Warn2("close(%d): %s", xfd->wfd, strerror(errno));
	 }
	 return STAT_RETRYLATER;
      }
      bytes += result;
      if (bytes == buflen) {
	 Debug1("received all "F_Zd" bytes", bytes);
	 break;
      }
      Debug2("received "F_Zd" bytes, waiting for "F_Zu" more bytes",
	     result, buflen-bytes);
   }
   if (result <= 0) {	/* we had a problem while reading socks answer */
      return STAT_RETRYLATER;	/* retry complete open cycle */
   }
   return STAT_OK;
}

static int xioopen_socks5_client(int argc, const char *argv[],
				 struct opt *opts, int xioflags,
				 xiofile_t *xxfd,
				 unsigned groups, int dummy1, int dummy2,
				 int dummy3) {
   /* we expect the form: host:port */
   xiosingle_t *xfd = &xxfd->stream;
   const char *targetname, *targetservice;
   int level;
   int result;

   if (argc != 3) {
      Error("SOCKS5 syntax: socks5-connect:<host>:<port>");	/*! UDP? */
      return STAT_NORETRY;
   }

   targetname = argv[1];
   targetservice = argv[2];

   applyopts(-1, opts, PH_INIT);
   applyopts_single(xfd, opts, PH_INIT);

#if 0
   result = _xioopen_socks5_prepare(a3, opts, &socksport, sockhead, &buflen);
   if (result != STAT_OK)  return result;
#endif

   if (xfd->rfd < 0) {
      Error("socks5 must be used as embedded address");
      return -1;
   }

   Notice2("opening connection to %s:%s using socks5",
	   targetname, targetservice);

   do {	/* loop over failed connect and socks-request attempts */

#if WITH_RETRY
      if (xfd->forever || xfd->retry) {
	 level = E_INFO;
      } else
#endif /* WITH_RETRY */
	 level = E_ERROR;

#if 0
      /* we try to resolve the target address _before_ connecting to the socks
	 server: this avoids unnecessary socks connects and timeouts */
      result =
	 _xioopen_socks5_connect0(xfd, targetname, sockhead, opts,
				  (ssize_t *)&buflen, level);
      switch (result) {
      case STAT_OK: break;
#if WITH_RETRY
      case STAT_RETRYLATER:
      case STAT_RETRYNOW:
	 if (xfd->forever || xfd->retry--) {
	    if (result == STAT_RETRYLATER)  Nanosleep(&xfd->intervall, NULL);
	    continue;
	 }
#endif /* WITH_RETRY */
      default:
	 return result;
      }
#endif

#if 0
     if (xfd->rfd < 0) {
      /* this cannot fork because we retrieved fork option above */
      result =
	 _xioopen_connect (xfd,
			   needbind?(struct sockaddr *)us:NULL, sizeof(*us),
			   (struct sockaddr *)them, sizeof(struct sockaddr_in),
			   opts, PF_INET, socktype, IPPROTO_TCP, lowport, level);
      switch (result) {
      case STAT_OK: break;
#if WITH_RETRY
      case STAT_RETRYLATER:
      case STAT_RETRYNOW:
	 if (xfd->forever || xfd->retry--) {
	    if (result == STAT_RETRYLATER)  Nanosleep(&xfd->intervall, NULL);
	    continue;
	 }
#endif /* WITH_RETRY */
      default:
	 return result;
      }
      xfd->rfd = xfd->wfd = xfd->fd;
     } else
#endif
	xfd->dtype = XIODATA_STREAM;

      result =
	_xioopen_socks5_connect(xfd, targetname, targetservice, opts, level);
      switch (result) {
      case STAT_OK: break;
#if WITH_RETRY
      case STAT_RETRYLATER:
      case STAT_RETRYNOW:
	 if (xfd->forever || xfd->retry--) {
	    if (result == STAT_RETRYLATER)  Nanosleep(&xfd->intervall, NULL);
	    continue;
	 }
#endif /* WITH_RETRY */
      default:
	 return result;
      }

      /*!*/
      applyopts(xfd->rfd, opts, PH_ALL);

      if ((result = _xio_openlate(xfd, opts)) < 0)
	 return result;

      {
	 break;
      }

   } while (true);	/* end of complete open loop - drop out on success */
   return 0;
}



/* perform socks5 client dialog on existing FD.
   Called within fork/retry loop, after connect() */
int _xioopen_socks5_connect(struct single *xfd,
			    const char *targetname, const char *targetservice,
			    struct opt *opts, int level) {
   uint16_t targetport;
   unsigned char sendbuff[SOCKS5_MAXLEN];  size_t sendlen;
   unsigned char recvbuff[SOCKS5_MAXLEN];
   struct socks5_method  *sendmethod;
   struct socks5_select  *recvselect;
   struct socks5_request *sendrequest;
   struct socks5_reply   *recvreply;
   size_t namelen;
   size_t addrlen;
   size_t readpos;
   char *emsg;
   int result;

   /* prepare */
   targetport    = parseport(targetservice, IPPROTO_TCP/*!*/);

   /* just the simplest authentications */
   sendmethod = (struct socks5_method *)sendbuff;
   sendmethod->version = 5;	/* protocol version */
   sendmethod->nmethods = 2;	/* number of proposed authentication types */
   sendmethod->methods[0] = SOCKS5_METHOD_NOAUTH;	/* no auth at all */
   sendmethod->methods[1] = SOCKS5_METHOD_USERPASS;	/* username/password */
   /*sendmethod->methods[2] = SOCKS5_METHOD_AVENTAIL;*/	/* Aventail Connect */
   sendlen = 2+sendmethod->nmethods;

   /* send socks header (target addr+port, +auth) */
   Info("sending socks5 identifier/method selection message");
   do {
      result = Write(xfd->wfd, sendmethod, sendlen);
   } while (result < 0 && errno == EINTR);
   if (result < 0) {
      Msg4(level, "write(%d, %p, "F_Zu"): %s",
	   xfd->wfd, sendmethod, sendlen, strerror(errno));
      if (Close(xfd->wfd) < 0) {
	 Warn2("close(%d): %s", xfd->wfd, strerror(errno));
      }
      if (Close(xfd->rfd) < 0) {
	 Warn2("close(%d): %s", xfd->rfd, strerror(errno));
      }
      return STAT_RETRYLATER;	/* retry complete open cycle */
   }

   Info1("waiting for socks5 select reply ("F_Zu" bytes)", SOCKS5_SELECT_LENGTH);
   if ((result =
	xiosocks5_recvbytes(xfd, recvbuff, SOCKS5_SELECT_LENGTH, level)) !=
       STAT_OK) {
      /* we had a problem while reading socks answer */
      /*! Close(); */
      return result;	/* ev. retry complete open cycle */
   }
   recvselect = (struct socks5_select *)recvbuff;
   Info2("socks5 select: {%u, %u}", recvselect->version, recvselect->method);
   if (recvselect->version != 5) {
      Error1("socks5: server protocol version is %u",
	     recvbuff[0]);
   }
   if (recvselect->method == SOCKS5_METHOD_NONE) {
      Error("socks5: server did not accept our authentication methods");
   }
   /*! check if selected methods is one of our proposals */

   switch (recvselect->method) {
   case SOCKS5_METHOD_NOAUTH:
      break;
   case SOCKS5_METHOD_USERPASS:
      if (xio_socks5_username_password(level, opts, xfd) < 0) {
	 Error("username/password not accepted");
      }
      break;
   default:
      Error("socks5 select: unimplemented authentication method selected");
      break;
   }

   sendrequest = (struct socks5_request *)sendbuff;
   sendrequest->version = SOCKS5_VERSION;
   sendrequest->command = SOCKS5_COMMAND_CONNECT;
   sendrequest->reserved = 0;
   sendrequest->addrtype = SOCKS5_ADDRTYPE_NAME;
   switch (sendrequest->addrtype) {
   case SOCKS5_ADDRTYPE_IPV4:
      break;
   case SOCKS5_ADDRTYPE_NAME:
      if ((namelen = strlen(targetname)) > 255) {
	 Error1("socks5: target name is longer than 255 bytes: \"%s\"",
		targetname);
      }
      sendrequest->destaddr[0] = strlen(targetname);
      *(sendrequest->destaddr+1) = '\0'; strncat(sendrequest->destaddr+1, targetname, sizeof(sendbuff)-5);
      break;
   case SOCKS5_ADDRTYPE_IPV6:
      break;
   default: Fatal("socks5: undefined address type in socks request");
   }
   *((uint16_t *)&sendrequest->destaddr[strlen(targetname)+1]) = targetport;
   sendlen = sizeof(struct socks5_request) + namelen + 2;

   /* send socks request (target addr+port, +auth) */
   Info("sending socks5 request selection");
   do {
      result = Write(xfd->wfd, sendrequest, sendlen);
   } while (result < 0 && errno == EINTR);
   if (result < 0) {
      Msg4(level, "write(%d, %p, "F_Zu"): %s",
	   xfd->wfd, sendmethod, sendlen, strerror(errno));
      if (Close(xfd->wfd) < 0) {
	 Warn2("close(%d): %s", xfd->wfd, strerror(errno));
      }
      if (Close(xfd->rfd) < 0) {
	 Warn2("close(%d): %s", xfd->rfd, strerror(errno));
      }
      return STAT_RETRYLATER;	/* retry complete open cycle */
   }

   Info("waiting for socks5 reply");
   recvreply = (struct socks5_reply *)recvbuff;
   if ((result =
	xiosocks5_recvbytes(xfd, recvbuff, SOCKS5_REPLY_LENGTH1, level))
       != STAT_OK) {
      /*! close... */
      return result;
   }
   if (recvreply->version != SOCKS5_VERSION) {
      Error1("socks5: version of reply is %d", recvreply->version);
   }
   switch (recvreply->reply) {
   case SOCKS5_REPLY_SUCCESS:     emsg = NULL; break;
   case SOCKS5_REPLY_FAILURE:     emsg = "general socks server failure"; break;
   case SOCKS5_REPLY_DENIED:      emsg = "connection not allowed by ruleset"; break;
   case SOCKS5_REPLY_NETUNREACH:  emsg = "network unreachable"; break;
   case SOCKS5_REPLY_HOSTUNREACH: emsg = "host unreachable"; break;
   case SOCKS5_REPLY_REFUSED:     emsg = "connection refused"; break;
   case SOCKS5_REPLY_TTLEXPIRED:  emsg = "TTL expired"; break;
   case SOCKS5_REPLY_CMDUNSUPP:   emsg = "command not supported"; break;
   case SOCKS5_REPLY_ADDRUNSUPP:  emsg = "address type not supported"; break;
   default:                       emsg = "undefined error code"; break;
   }
   if (recvreply->reply != SOCKS5_REPLY_SUCCESS) {
      Error1("socks5 reply: %s", emsg);
   }
   
   switch (recvreply->addrtype) {
   case SOCKS5_ADDRTYPE_IPV4:
      addrlen = 4;	/*!*/
      if ((result =
	   xiosocks5_recvbytes(xfd, recvbuff+SOCKS5_REPLY_LENGTH1, addrlen,
			       level))
	   != STAT_OK) {
	 /*! close... */
	 return result;
      }
      readpos = sizeof(recvreply)-1+4;
      break;
   case SOCKS5_ADDRTYPE_NAME:
      /* read 1 byte containing domain name length */
      if ((result =
	   xiosocks5_recvbytes(xfd, recvbuff+SOCKS5_REPLY_LENGTH1, 1, level))
	  != STAT_OK) {
	 /*! close... */
	 return result;
      }
      /* read domain name */
      if ((result =
	   xiosocks5_recvbytes(xfd, recvbuff+SOCKS5_REPLY_LENGTH1+1,
			       recvreply->destaddr[0], level))
	  != STAT_OK) {
	 /*! close... */
	 return result;
      }
      readpos = sizeof(recvreply)+recvreply->destaddr[0];
      addrlen = recvreply->destaddr[0]+1;
      break;
   case SOCKS5_ADDRTYPE_IPV6:
      addrlen = 16;	/*!*/
      if ((result =
	   xiosocks5_recvbytes(xfd, recvbuff+SOCKS5_REPLY_LENGTH1, addrlen,
			      level))
	  == EOF) {
	 /*! close... */
	 return result;
      }
      readpos = sizeof(recvreply)-1+4;
      break;
   default: Error("socks5: undefined address type in answer");
      addrlen = 0;
      readpos = sizeof(recvreply)-1;
   }
   if ((result =
	xiosocks5_recvbytes(xfd, recvbuff+readpos, 2, level))
        != STAT_OK) {
      /*! close... */
      return result;
   }

   /*! Infos(...); */
   return STAT_OK;
}

int xio_socks5_username_password(int level, struct opt *opts,
				 struct single *xfd) {
   /*!!! */
   unsigned char sendbuff[513];
   unsigned char *pos;
   char *username = NULL;
   char *password = NULL;
   unsigned char recvbuff[2];
   struct socks5_userpass_reply *reply;
   int result;

   retropt_string(opts, OPT_SOCKS5_USERNAME, (char **)&username);
   if (username == NULL) {
      Error("socks5: username required");
      return STAT_NORETRY;
   }
   retropt_string(opts, OPT_SOCKS5_PASSWORD, (char **)&password);
   if (password == NULL) {
      Error("socks5: password required");
      return STAT_NORETRY;
   }

   pos = sendbuff;
   *pos++ = SOCKS5_USERPASS_VERSION;
   *pos++ = strlen(username);
   *pos = '\0'; strncat(pos, username, 255);
   pos += strlen(username);
   *pos++ = strlen(password);
   *pos = '\0'; strncat(pos, password, 255);
   pos += strlen(password);

   result =
     xio_socks5_dialog(level, xfd, sendbuff, pos-sendbuff,
		       recvbuff, 2,
		       "username/password authentication");
   if (result != STAT_OK) {
      Msg(level, "socks5 username/password dialog failed");
      return result;
   }
   Info("socks5 username/password authentication succeeded");
   reply = (struct socks5_userpass_reply *)recvbuff;
   if (reply->version != SOCKS5_USERPASS_VERSION) {
      Msg(level, "socks5 username/password authentication version mismatch");
      return STAT_NORETRY;
   }
   if (reply->status != SOCKS5_REPLY_SUCCESS) {
      Msg(level, "socks5 username/password authentication failure");
      return STAT_RETRYLATER;
   }
   return STAT_OK;
}

int xio_socks5_dialog(int level, struct single *xfd,
		      unsigned char *sendbuff, size_t sendlen,
		      unsigned char *recvbuff, size_t recvlen,
		      const char *descr /* identifier/method selection */) {
   int result;

   /* send socks header (target addr+port, +auth) */
   Info1("sending socks5 %s message", descr);
   do {
      result = Write(xfd->wfd, sendbuff, sendlen);
   } while (result < 0 && errno == EINTR);
   if (result < 0) {
      Msg4(level, "write(%d, %p, "F_Zu"): %s",
	   xfd->wfd, sendbuff, sendlen, strerror(errno));
      if (Close(xfd->wfd) < 0) {
	 Warn2("close(%d): %s", xfd->wfd, strerror(errno));
      }
      if (Close(xfd->rfd) < 0) {
	 Warn2("close(%d): %s", xfd->rfd, strerror(errno));
      }
      return STAT_RETRYLATER;	/* retry complete open cycle */
   }

   Info2("waiting for socks5 %s reply ("F_Zu" bytes)",
	 descr, recvlen);
   if ((result =
	xiosocks5_recvbytes(xfd, recvbuff, recvlen, level))
       == EOF) {
      /* we had a problem while reading socks answer */
      /*! Close(); */
      return result;	/* retry complete open cycle */
   }
#if 0
   replyversion = ((struct socks5_version *)recvbuff)->version;
   Info1("socks5 server reply version: {%u}", replyversion);
   if (replyversion != SOCKS5_VERSION) {
      Error1("socks5: server protocol version is %u",
	     replyversion);
   }
#endif
   return 0;
}

#endif /* WITH_SOCKS5 */

