/*
 * Name: sslproxy.c
 * Project: SSL Proxy
 * Author: Christian Starkjohann <cs@obdev.at>
 * Creation Date: 1998-06-12
 * Tabsize: 4
 * Copyright: (c) 1998 by Christian Starkjohann, all rights reserved.
 *    sslproxy is distributed under the terms of the GNU General Public
 *    License (GPL) version 2.0.
 * This Revision: $Id: sslproxy.c,v 1.2 2000/01/29 16:35:49 cs Exp $
 */


#include "stdheaders.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include "cmdline.h"

/* ------------------------------------------------------------------------- */
#define SRV_SSL_V23	0
#define SRV_SSL_V2	1
#define SRV_SSL_V3	2
#define SRV_SSL_TLS1	3
/* ------------------------------------------------------------------------- */

char buffer[80];   //Buffer to hold the ssl connection data 
typedef struct options
{
  char isServer;
  char netbiosAutomatic;
  char sslCompat;
  char requirePeerCert;
  char protocol;
  char certfile[MAXPATHLEN];
  char keyfile[MAXPATHLEN];
  char verifyDir[MAXPATHLEN];
  char verifyFile[MAXPATHLEN];
  
  unsigned localIp;
  int localPort;
  unsigned remoteIp;
  int remotePort;
}options_t;

typedef struct descriptor
{
  int	fd;
  SSL	*ssl;
}descriptor_t;

static SSL_CTX  *sslContext = NULL;

static int ssl_verify_cb(int ok, X509_STORE_CTX *ctx)
{
  char buffer[256];
  
  X509_NAME_oneline(X509_get_issuer_name(ctx->current_cert), buffer, sizeof(buffer));
  if(ok)
    {
      fprintf(stderr, "SSL: Certificate OK: %s\n", buffer);
    }
  else
    {
      switch (ctx->error)
	{
        case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
	  fprintf(stderr, "SSL: Cert error: CA not known: %s\n", buffer);
	  break;
        case X509_V_ERR_CERT_NOT_YET_VALID:
	  fprintf(stderr, "SSL: Cert error: Cert not yet valid: %s\n", buffer);
	  break;
        case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
	  fprintf(stderr, "SSL: Cert error: illegal \'not before\' field: %s\n", buffer);
	  break;
        case X509_V_ERR_CERT_HAS_EXPIRED:
	  fprintf(stderr, "SSL: Cert error: Cert expired: %s\n", buffer);
	  break;
        case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
	  fprintf(stderr, "SSL: Cert error: invalid \'not after\' field: %s\n", buffer);
	  break;
        default:
	  fprintf(stderr, "SSL: Cert error: unknown error %d in %s\n", ctx->error, buffer);
	  break;
        }
    }
  return ok;
}

static RSA  *ssl_temp_rsa_cb(SSL *ssl, int export, int keylength)
{
  static RSA  *rsa = NULL;
  
  if(rsa == NULL)
    rsa = RSA_generate_key(1024, RSA_F4, NULL, NULL);
  return rsa;
}

static int sslutil_init(options_t *opt)
{
  int err;
  char *certfile, *keyfile, *cacertDir, *cacertFile;
  
  SSL_load_error_strings();
  SSLeay_add_ssl_algorithms();
  switch(opt->protocol)
    {
    case SRV_SSL_V2:	sslContext = SSL_CTX_new(SSLv2_method());	break;
    case SRV_SSL_V3:	sslContext = SSL_CTX_new(SSLv3_method());	break;
    default:
    case SRV_SSL_V23:	sslContext = SSL_CTX_new(SSLv23_method());	break;
    case SRV_SSL_TLS1:	sslContext = SSL_CTX_new(TLSv1_method());	break;
    }
  if(sslContext == NULL)
    {
      err = ERR_get_error();
      fprintf(stderr, "SSL: Error allocating context: %s\n", ERR_error_string(err, NULL));
      exit(1);
    }
  if(opt->sslCompat)
    {
      SSL_CTX_set_options(sslContext, SSL_OP_ALL);
    }
  certfile = opt->certfile;
  if((certfile == NULL || *certfile == 0) && opt->isServer)
    {
      fprintf(stderr, "SSL: No cert file specified in config file!\n");
      fprintf(stderr, "The server MUST have a certificate!\n");
      exit(1);
    }
  keyfile = opt->keyfile;
  if(keyfile == NULL || *keyfile == 0)
    keyfile = certfile;
  if(certfile != NULL && *certfile != 0)
    {
      if(!SSL_CTX_use_certificate_file(sslContext, certfile, SSL_FILETYPE_PEM))
	{
	  err = ERR_get_error();
	  fprintf(stderr, "SSL: error reading certificate from file %s: %s\n", certfile, ERR_error_string(err, NULL));
	  exit(1);
	}
      if(!SSL_CTX_use_PrivateKey_file(sslContext, keyfile, SSL_FILETYPE_PEM))
	{
	  err = ERR_get_error();
	  fprintf(stderr, "SSL: error reading private key from file %s: %s\n", keyfile, ERR_error_string(err, NULL));
	  exit(1);
	}
      if(!SSL_CTX_check_private_key(sslContext)){
	fprintf(stderr, "SSL: Private key does not match public key in cert!\n");
	exit(1);
      }
    }
  cacertDir = opt->verifyDir;
  cacertFile = opt->verifyFile;
  if(cacertDir != NULL && *cacertDir == 0)
    cacertDir = NULL;
  if(cacertFile != NULL && *cacertFile == 0)
    cacertFile = NULL;
  if(cacertDir != NULL || cacertFile != NULL)
    {
      if(!SSL_CTX_load_verify_locations(sslContext, cacertFile, cacertDir))
	{
	  err = ERR_get_error();
	  fprintf(stderr, "SSL: Error error setting CA cert locations: %s\n", ERR_error_string(err, NULL));
	  cacertFile = cacertDir = NULL;
	}
    }
  if(cacertDir == NULL && cacertFile == NULL)
    {	/* no verify locations loaded */
      fprintf(stderr, "SSL: No verify locations, trying default\n");
      if(!SSL_CTX_set_default_verify_paths(sslContext))
	{
	  err = ERR_get_error();
	  fprintf(stderr, "SSL: Error error setting default CA cert location: %s\n", ERR_error_string(err, NULL));
	  fprintf(stderr, "continuing anyway...\n");
	}
    }
  SSL_CTX_set_tmp_rsa_callback(sslContext, ssl_temp_rsa_cb);
  if(opt->requirePeerCert){
    SSL_CTX_set_verify(sslContext, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, ssl_verify_cb);
  }
  else
    {
      SSL_CTX_set_verify(sslContext, SSL_VERIFY_NONE, ssl_verify_cb);
    }
#if 1 // don't know what this is good for, but s_server in SSLeay does it, too 
  if(opt->isServer)
    {
      SSL_CTX_set_client_CA_list(sslContext, SSL_load_client_CA_file(certfile));
    }
#endif
  return 0;
}

/* ------------------------------------------------------------------------- */

static int sslutil_accept(descriptor_t *d)
{
  int     err;
  SSL		*ssl;
  
  if((ssl = SSL_new(sslContext)) == NULL)
    {
      err = ERR_get_error();
      fprintf(stderr, "SSL: Error allocating handle: %s\n", ERR_error_string(err, NULL));
      return -1;
    }
    SSL_set_fd(ssl, d->fd);
    if(SSL_accept(ssl) <= 0)
      {
        err = ERR_get_error();
        fprintf(stderr, "SSL: Error accepting on socket: %s\n", ERR_error_string(err, NULL));
        return -1;
      }
    fprintf(stderr, "SSL: negotiated cipher: %s\n", SSL_get_cipher(ssl));
    d->ssl = ssl;
    return 0;
}

/* ------------------------------------------------------------------------- */

static int sslutil_connect(descriptor_t *d)
{
  int err;
  SSL *ssl;
  
  if((ssl = SSL_new(sslContext)) == NULL)
    {
      err = ERR_get_error();
      fprintf(stderr, "SSL: Error allocating handle: %s\n", ERR_error_string(err, NULL));
      return -1;
    }
  SSL_set_fd(ssl, d->fd);
  if(SSL_connect(ssl) <= 0)
    {
      err = ERR_get_error();
      fprintf(stderr, "SSL: Error conencting socket: %s\n", ERR_error_string(err, NULL));
      return -1;
    }
  fprintf(stderr, "SSL: negotiated cipher: %s\n", SSL_get_cipher(ssl));
  d->ssl = ssl;
  return 0;
}

/* ------------------------------------------------------------------------- */

static void netbTransmit(descriptor_t *d, char *buf)
{
  int len, written;
  
  len = (unsigned char)buf[3];
  len |= (int)((unsigned char)buf[2]) << 8;
  len += 4;	/* length of header */
  while(len > 0){
    if(d->ssl != NULL)
      {
	written = SSL_write(d->ssl, buf, len);
      }
    else
      {
	written = write(d->fd, buf, len);
      }
    if(written < 0)
      {
	if(errno == EINTR)	/* interrupted system call */
	  continue;
	perror("write");
	exit(1);
      }
    if(written == 0)
      {
	fprintf(stderr, "broken pipe in write\n");
	exit(0);
      }
    buf += written;
    len -= written;
  }
}

static void readWithLen(descriptor_t *d, char *buf, int len)
{
  int rval, remaining = len;
  
  while(remaining > 0)
    {
      if(d->ssl != NULL)
	{
	  rval = SSL_read(d->ssl, buf, remaining);
	}
      else
	{
	  rval = read(d->fd, buf, remaining);
	}
      if(rval < 0)
	{
	  if(errno == EINTR)	/* interrupted system call */
	    continue;
	  perror("read");
	  exit(1);
	}
      if(rval == 0)
	{
	  fprintf(stderr, "broken pipe in read\n");
	  exit(0);
	}
      buf += rval;
      remaining -= rval;
    }
}

static int netbReceive(descriptor_t *d, char *buf, int buflen)
{
  int		len;
  
  readWithLen(d, buf, 4);
  len = (unsigned char)buf[3];
  len |= (int)((unsigned char)buf[2]) << 8;
  if(len + 4 > buflen)
    {
      fprintf(stderr, "received block too long\n");
      exit(1);
    }
  readWithLen(d, buf + 4, len);
  return len + 4;
}

/* ------------------------------------------------------------------------- */

static int sslutil_negotiate_ssl(descriptor_t *d, int msg_type)
{
  unsigned char buf[5] = {0x83, 0, 0, 1, 0x81};
  
  if(msg_type != 0x81)
    { /* first packet must be a session request */
      fprintf(stderr, "Client did not use session setup; access denied\n");
      netbTransmit(d, (char *)buf);
      return -1;
    }
  buf[4] = 0x8e;  /* negative session response: use SSL */
  netbTransmit(d, (char *)buf);
  if(sslutil_accept(d) != 0)
    {
      fprintf(stderr, "Client failed SSL negotiation!\n");
      return -1;
    }
  return 1;
}

/* ------------------------------------------------------------------------- */

static void shovel(descriptor_t *fromFd, descriptor_t *toFd)
{
  int len, written, rval;
  char buffer[8192];
  
 retryRead:
  if(fromFd->ssl != NULL)
    {
      len = SSL_read(fromFd->ssl, buffer, sizeof(buffer));
    }
  else
    {
      len = read(fromFd->fd, buffer, sizeof(buffer));
    }
  if(len == 0)
    {	/* broken pipe */
      fprintf(stderr, "client: broken pipe (read)\n");
      exit(0);
    }
  if(len < 0)
    {
      if(errno == EINTR)	/* interrupted system call */
	goto retryRead;
      perror("read");
      return;
    }
  for(written=0; written<len; written+=rval)
    {
      if(toFd->ssl != NULL)
	{
	  rval = SSL_write(toFd->ssl, buffer + written, len - written);
	}
      else
	{
	  rval = write(toFd->fd, buffer + written, len - written);
	}
      if(rval == 0)
	{	/* broken pipe */
	  fprintf(stderr, "client: broken pipe (write)\n");
	  exit(0);
	}
      if(rval < 0)
	{
	  rval = 0;			/* if we continue, don't subtract from 'written' */
	  if(errno == EINTR)	/* interrupted system call */
	    continue;
	  perror("write");
	  break;
	}
    }
}

/* ------------------------------------------------------------------------- */

static void client(int clientFd, options_t *opt)
{
  int serverFd, maxFd, nfd;
  struct sockaddr_in sockAddr;
  fd_set writeFds, readFds, excFds;
  descriptor_t srvDesc, clientDesc;
  unsigned char	buf[4096];
  
  clientDesc.fd = clientFd;
  clientDesc.ssl = NULL;
  if(opt->isServer)
    {
      if(opt->netbiosAutomatic)
	{
	  netbReceive(&clientDesc, (char *)buf, sizeof(buf));
	  if(sslutil_negotiate_ssl(&clientDesc, buf[0]) < 0)
	    {
	      fprintf(stderr, "failed client SSL negotiation\n");
	      exit(1);
	    }
	}
      else
	{
	  if(sslutil_accept(&clientDesc) != 0)
	    {
	      fprintf(stderr, "Client failed SSL negotiation!\n");
	      exit(1);
	    }
	}
    }
  if((serverFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    {
      perror("client: socket");
      exit(1);
    }
  sockAddr.sin_family = AF_INET;
  sockAddr.sin_addr.s_addr = opt->remoteIp;
  sockAddr.sin_port = htons(opt->remotePort);
 retryConnect:
  if(connect(serverFd, (struct sockaddr *)&sockAddr, sizeof(sockAddr)) != 0)
    {
      if(errno == EINTR)	/* interrupted system call */
	goto retryConnect;
      perror("connect");
      exit(1);
    }
  srvDesc.fd = serverFd;
  srvDesc.ssl = NULL;
  if(!opt->isServer)
    {
      if(opt->netbiosAutomatic)
	{
	  unsigned char	txPacket[4] = {0x81, 0, 0, 0};
	  netbTransmit(&srvDesc, (char *)txPacket);			/* send dummy session request */
	  if(netbReceive(&srvDesc, (char *)buf, sizeof(buf)) != 5 || buf[0] != 0x83 || buf[4] != 0x8e){
	    fprintf(stderr, "Server does not require encryption! Connect without proxy!\n");
	    exit(1);
	  }
	}
      if(sslutil_connect(&srvDesc) < 0)
	{
	  fprintf(stderr, "failed SSL connection to server\n");
	  exit(1);
	}
    }
  maxFd = clientFd > serverFd ? clientFd : serverFd;
  for(;;)
    {
      FD_ZERO(&writeFds);
      FD_ZERO(&readFds);
      FD_ZERO(&excFds);
      FD_SET(serverFd, &readFds);
      FD_SET(clientFd, &readFds);
      nfd = select(maxFd + 1, &readFds, &writeFds, &excFds, NULL);
      if(nfd < 0)
	{
	  if(errno == EINTR)	/* interrupted system call */
	    continue;
	  perror("select");
	  break;
	}
      if(FD_ISSET(serverFd, &readFds))
	{
	  shovel(&srvDesc, &clientDesc);
	}
      if(FD_ISSET(clientFd, &readFds))
	{
	  shovel(&clientDesc, &srvDesc);
	}
    }
}

#ifdef __CYGWIN32__
extern void cygwin32_conv_to_full_posix_path(const char *src_path, char *posix_path);
static void getPath(char *dest, char *src)
{
  cygwin32_conv_to_full_posix_path(src, dest);
}
#else	/* __CYGWIN32__ */
static void getPath(char *dest, char *src)
{
  strncpy(dest, src, MAXPATHLEN);
  if(strlen(src) >= MAXPATHLEN)
    dest[MAXPATHLEN - 1] = 0;
}
#endif	/* __CYGWIN32__ */

static void checkChildstatus(int signr)
{
  wait(NULL);
}

static options_t options[1];

int make_sslproxy(struct gengetopt_args_info *args_info)
{
  options_t *opt = options;
  int sockFd, clientFd;
  struct sockaddr_in sockAddr;
  signal(SIGCHLD, checkChildstatus);
  memset(opt, 0, sizeof(options_t));

  /* ------- Will come back and clean up stuctures :) ------------*/

  opt->remoteIp = args_info->sslRedirectIp_arg;
  opt->localIp = args_info->sslLocalIp_arg; 
  if(args_info->sslLocalPort_given)
    {
      opt->localPort = args_info->sslLocalPort_arg;             
      fprintf(stderr, "Send Connection to this Listening Port: %d \n", opt->localPort);
    }

  if(args_info->standalone_given == 1)
    {
      opt->remotePort = args_info->standalone_arg;            
    }
  else
    {
      opt->remotePort = args_info->sslRedirectPort_arg;            
    }
  opt->isServer = args_info->sslServermode_flag;               
  //  opt->netbiosAutomatic = 1;	                       
  if(args_info->certFile_given == 1)
    {
      getPath(opt->certfile, args_info->certFile_arg);     
    }
  if(args_info->keyFile_given == 1)
    {
      getPath(opt->keyfile, args_info->keyFile_arg);
    }
  
  if(args_info->verifyDir_given == 1)
    {
      getPath(opt->verifyDir, args_info->verifyDir_arg);	
    }
  if(args_info->verifyFile_given == 1)
    {
      getPath(opt->verifyFile, args_info->verifyFile_arg);
    }
  if(args_info->peerCert_given == 1)
    {
      opt->requirePeerCert = args_info->peerCert_flag;
    }
  
  opt->sslCompat = args_info->sslClientmode_flag; 
  
  if(args_info->sslProto_given == 1)
    {
      if(strcmp(args_info->sslProto_arg, "ssl23") == 0)
	opt->protocol = SRV_SSL_V23;
      else if(strcmp(args_info->sslProto_arg, "ssl2") == 0)
	opt->protocol = SRV_SSL_V2;
      else if(strcmp(args_info->sslProto_arg, "ssl3") == 0)
	opt->protocol = SRV_SSL_V3;
      else if(strcmp(args_info->sslProto_arg, "tls1") == 0)
	opt->protocol = SRV_SSL_TLS1;
      else
	{
	  fprintf(stderr, "protocol version %s not known\n", args_info->sslProto_arg);
	  exit(1);
	}
    }
  if(opt->remoteIp == 0)
    {
      fprintf(stderr, "No remote address given\n");
      //      printHelp(progname);
      exit(1);
    }
  sslutil_init(opt);
  if((sockFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0){
    perror("socket");
    exit(1);
  }
  memset(&sockAddr, 0, sizeof(sockAddr));
  sockAddr.sin_family = AF_INET;
  sockAddr.sin_addr.s_addr = opt->localIp;
  sockAddr.sin_port = htons(opt->localPort);
  if(bind(sockFd, (struct sockaddr *) &sockAddr, sizeof(sockAddr)) < 0)
    {
      perror("bind");
      exit(1);
    }
  if(listen(sockFd, 10) < 0)
    {	/* a queue of 10 pending connects should suffice... */
      perror("listen");
      exit(1);
    }
  fprintf(stderr, "proxy ready, listening for connections\n");
  for(;;)
    {
      clientFd = accept(sockFd, NULL, NULL);
      fprintf(stderr, "connection on fd=%d\n", clientFd);
      if(clientFd < 0)
	{
	  if(errno == EINTR)	/* interrupted system call */
	    continue;
	  perror("accept");
	  exit(1);
	}
      if(fork() == 0)
	{	
	  //if we are the child 
	  close(sockFd); //client does not need listening socket 
	  client(clientFd, opt);
	  exit(0);
	}
      else
	{
	  close(clientFd); //parent does not need new fd 
	}
    }
}
