/*
     This file is part of GNUnet.
     Copyright (C) 2009-2013 GNUnet e.V.

     GNUnet is free software: you can redistribute it and/or modify it
     under the terms of the GNU Affero General Public License as published
     by the Free Software Foundation, either version 3 of the License,
     or (at your option) any later version.

     GNUnet is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     Affero General Public License for more details.

     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * @file util/socks.c
 * @brief  SOCKS5 connection support
 * @author Jeffrey Burdges
 *
 * These routines should be called only on newly active connections.
 */
#include "platform.h"
#include "gnunet_util_lib.h"


#define LOG(kind, ...) GNUNET_log_from (kind, "util-socks", __VA_ARGS__)

#define LOG_STRERROR(kind, syscall) \
  GNUNET_log_from_strerror (kind, "util-socks", syscall)


/* SOCKS5 authentication methods */
#define SOCKS5_AUTH_REJECT 0xFF /* No acceptable auth method */
#define SOCKS5_AUTH_NOAUTH 0x00 /* without authentication */
#define SOCKS5_AUTH_GSSAPI 0x01 /* GSSAPI */
#define SOCKS5_AUTH_USERPASS 0x02 /* User/Password */
#define SOCKS5_AUTH_CHAP 0x03 /* Challenge-Handshake Auth Proto. */
#define SOCKS5_AUTH_EAP 0x05 /* Extensible Authentication Proto. */
#define SOCKS5_AUTH_MAF 0x08 /* Multi-Authentication Framework */


/* SOCKS5 connection responces */
#define SOCKS5_REP_SUCCEEDED 0x00 /* succeeded */
#define SOCKS5_REP_FAIL 0x01 /* general SOCKS serer failure */
#define SOCKS5_REP_NALLOWED 0x02 /* connection not allowed by ruleset */
#define SOCKS5_REP_NUNREACH 0x03 /* Network unreachable */
#define SOCKS5_REP_HUNREACH 0x04 /* Host unreachable */
#define SOCKS5_REP_REFUSED 0x05 /* connection refused */
#define SOCKS5_REP_EXPIRED 0x06 /* TTL expired */
#define SOCKS5_REP_CNOTSUP 0x07 /* Command not supported */
#define SOCKS5_REP_ANOTSUP 0x08 /* Address not supported */
#define SOCKS5_REP_INVADDR 0x09 /* Inalid address */

const char *
SOCKS5_REP_names (int rep)
{
  switch (rep)
  {
  case SOCKS5_REP_SUCCEEDED:
    return "succeeded";

  case SOCKS5_REP_FAIL:
    return "general SOCKS server failure";

  case SOCKS5_REP_NALLOWED:
    return "connection not allowed by ruleset";

  case SOCKS5_REP_NUNREACH:
    return "Network unreachable";

  case SOCKS5_REP_HUNREACH:
    return "Host unreachable";

  case SOCKS5_REP_REFUSED:
    return "connection refused";

  case SOCKS5_REP_EXPIRED:
    return "TTL expired";

  case SOCKS5_REP_CNOTSUP:
    return "Command not supported";

  case SOCKS5_REP_ANOTSUP:
    return "Address not supported";

  case SOCKS5_REP_INVADDR:
    return "Invalid address";

  default:
    return NULL;
  }
};


/**
 * Encode a string for the SOCKS5 protocol by prefixing it a byte stating its
 * length and stripping the trailing zero byte.  Truncates any string longer
 * than 255 bytes.
 *
 * @param b buffer to contain the encoded string
 * @param s string to encode
 * @return pointer to the end of the encoded string in the buffer
 */
unsigned char *
SOCK5_proto_string (unsigned char *b, const char *s)
{
  size_t l = strlen (s);

  if (l > 255)
  {
    LOG (GNUNET_ERROR_TYPE_WARNING,
         "SOCKS5 cannot handle hostnames, usernames, or passwords over 255 bytes, truncating.\n");
    l = 255;
  }
  *(b++) = (unsigned char) l;
  memcpy (b, s, l);
  return b + l;
}


#define SOCKS5_step_greet 0
#define SOCKS5_step_auth 1
#define SOCKS5_step_cmd 2
#define SOCKS5_step_done 3

/**
 * State of the SOCKS5 handshake.
 */
struct GNUNET_SOCKS_Handshake
{
  /**
   * Connection handle used for SOCKS5
   */
  struct GNUNET_CONNECTION_Handle *socks5_connection;

  /**
   * Connection handle initially returned to client
   */
  struct GNUNET_CONNECTION_Handle *target_connection;

  /**
   * Transmission handle on socks5_connection.
   */
  struct GNUNET_CONNECTION_TransmitHandle *th;

  /**
   * Our stage in the SOCKS5 handshake
   */
  int step;

  /**
   * Precomputed SOCKS5 handshake ouput buffer
   */
  unsigned char outbuf[1024];

  /**
   * Pointers delineating protoocol steps in the outbut buffer
   */
  unsigned char *(outstep[4]);

  /**
   * SOCKS5 handshake input buffer
   */
  unsigned char inbuf[1024];

  /**
   * Pointers delimiting the current step in the input buffer
   */
  unsigned char *instart;
  unsigned char *inend;
};


/* Regitering prototypes */

void
register_reciever (struct GNUNET_SOCKS_Handshake *ih, int want);

/* In fact, the client sends first rule in GNUnet suggests one could take
 * large mac read sizes without fear of screwing up the proxied protocol,
 * but we make a proper SOCKS5 client. */
#define register_reciever_wants(ih) ((SOCKS5_step_cmd == ih->step) ? 10 : 2)


struct GNUNET_CONNECTION_TransmitHandle *
register_sender (struct GNUNET_SOCKS_Handshake *ih);


/**
 * Conclude the SOCKS5 handshake successfully.
 *
 * @param ih SOCKS5 handshake, consumed here.
 * @param c open unused connection, consumed here.
 * @return Connection handle that becomes usable when the handshake completes.
 */
void
SOCKS5_handshake_done (struct GNUNET_SOCKS_Handshake *ih)
{
  GNUNET_CONNECTION_acivate_proxied (ih->target_connection);
}


/**
 * Read one step in the SOCKS5 handshake.
 *
 * @param ih SOCKS5 Handshake
 */
void
SOCKS5_handshake_step (struct GNUNET_SOCKS_Handshake *ih)
{
  unsigned char *b = ih->instart;
  size_t available = ih->inend - b;

  int want = register_reciever_wants (ih);

  if (available < want)
  {
    register_reciever (ih, want - available);
    return;
  }
  GNUNET_assert (SOCKS5_step_done > ih->step && ih->step >= 0);
  switch (ih->step)
  {
  case SOCKS5_step_greet:   /* SOCKS5 server's greeting */
    if (b[0] != 5)
    {
      LOG (GNUNET_ERROR_TYPE_ERROR, "Not a SOCKS5 server\n");
      GNUNET_assert (0);
    }
    switch (b[1])
    {
    case SOCKS5_AUTH_NOAUTH:
      ih->step = SOCKS5_step_cmd;     /* no authentication to do */
      break;

    case SOCKS5_AUTH_USERPASS:
      ih->step = SOCKS5_step_auth;
      break;

    case SOCKS5_AUTH_REJECT:
      LOG (GNUNET_ERROR_TYPE_ERROR, "No authentication method accepted\n");
      return;

    default:
      LOG (GNUNET_ERROR_TYPE_ERROR,
           "Not a SOCKS5 server / Nonsensical authentication\n");
      return;
    }
    b += 2;
    break;

  case SOCKS5_step_auth:   /* SOCKS5 server's responce to authentication */
    if (b[1] != 0)
    {
      LOG (GNUNET_ERROR_TYPE_ERROR, "SOCKS5 authentication failed\n");
      GNUNET_assert (0);
    }
    ih->step = SOCKS5_step_cmd;
    b += 2;
    break;

  case SOCKS5_step_cmd:   /* SOCKS5 server's responce to command */
    if (b[0] != 5)
    {
      LOG (GNUNET_ERROR_TYPE_ERROR, "SOCKS5 protocol error\n");
      GNUNET_assert (0);
    }
    if (0 != b[1])
    {
      LOG (GNUNET_ERROR_TYPE_ERROR,
           "SOCKS5 connection error : %s\n",
           SOCKS5_REP_names (b[1]));
      return;
    }
    b += 3;
    /* There is no reason to verify host and port afaik. */
    switch (*(b++))
    {
    case 1:     /* IPv4 */
      b += sizeof(struct in_addr);     /* 4 */
      break;

    case 4:     /* IPv6 */
      b += sizeof(struct in6_addr);     /* 16 */
      break;

    case 3:     /* hostname */
      b += *b;
      break;
    }
    b += 2;   /* port */
    if (b > ih->inend)
    {
      register_reciever (ih, b - ih->inend);
      return;
    }
    ih->step = SOCKS5_step_done;
    LOG (GNUNET_ERROR_TYPE_DEBUG,
         "SOCKS5 server : %s\n",
         SOCKS5_REP_names (b[1]));
    ih->instart = b;
    SOCKS5_handshake_done (ih);
    return;

  case SOCKS5_step_done:
    GNUNET_assert (0);
  }
  ih->instart = b;
  /* Do not reschedule the sender unless we're done reading.
   * I imagine this lets us avoid ever cancelling the transmit handle. */
  register_sender (ih);
}


/**
 * Callback to read from the SOCKS5 proxy.
 *
 * @param client the service
 * @param handler function to call with the message
 * @param handler_cls closure for @a handler
 */
void
reciever (void *cls,
          const void *buf,
          size_t available,
          const struct sockaddr *addr,
          socklen_t addrlen,
          int errCode)
{
  struct GNUNET_SOCKS_Handshake *ih = cls;

  GNUNET_assert (&ih->inend[available] < &ih->inbuf[1024]);
  GNUNET_memcpy (ih->inend, buf, available);
  ih->inend += available;
  SOCKS5_handshake_step (ih);
}


/**
 * Register callback to read from the SOCKS5 proxy.
 *
 * @param client the service
 * @param handler function to call with the message
 * @param handler_cls closure for @a handler
 */
void
register_reciever (struct GNUNET_SOCKS_Handshake *ih, int want)
{
  GNUNET_CONNECTION_receive (ih->socks5_connection,
                             want,
                             GNUNET_TIME_relative_get_minute_ (),
                             &reciever,
                             ih);
}


/**
 * Register SOCKS5 handshake sender
 *
 * @param cls closure (SOCKS handshake)
 * @param size number of bytes available in @a buf
 * @param buf where the callee should write the message
 * @return number of bytes written to @a buf
 */
size_t
transmit_ready (void *cls, size_t size, void *buf)
{
  struct GNUNET_SOCKS_Handshake *ih = cls;

  /* connection.c has many routines that call us with buf == NULL :
   * signal_transmit_error() - DNS, etc. active
   *   connect_fail_continuation()
   *     connect_probe_continuation() - timeout
   *     try_connect_using_address() - DNS failure/timeout
   *     transmit_timeout() - retry failed?
   * GNUNET_CONNECTION_notify_transmit_ready() can schedule :
   *   transmit_timeout() - DNS still working
   *   connect_error() - DNS done but no socket?
   * transmit_ready() - scheduler shutdown or timeout, or signal_transmit_error()
   * We'd need to dig into the scheduler to guess at the reason, as
   * connection.c tells us nothing itself, but mostly its timouts.
   * Initially, we'll simply ignore this and leave massive timeouts, but
   * maybe that should change for error handling pruposes.  It appears that
   * successful operations, including DNS resolution, do not use this.  */if (NULL == buf)
  {
    if (0 == ih->step)
    {
      LOG (GNUNET_ERROR_TYPE_WARNING,
           "Timeout contacting SOCKS server, retrying indefinitely, but probably hopeless.\n");
      register_sender (ih);
    }
    else
    {
      LOG (GNUNET_ERROR_TYPE_ERROR,
           "Timeout during mid SOCKS handshake (step %u), probably not a SOCKS server.\n",
           ih->step);
      GNUNET_break (0);
    }
    return 0;
  }

  GNUNET_assert ((1024 >= size) && (size > 0));
  GNUNET_assert ((SOCKS5_step_done > ih->step) && (ih->step >= 0));
  unsigned char *b = ih->outstep[ih->step];
  unsigned char *e = ih->outstep[ih->step + 1];
  GNUNET_assert (e <= &ih->outbuf[1024]);
  unsigned int l = e - b;
  GNUNET_assert (size >= l);
  GNUNET_memcpy (buf, b, l);
  register_reciever (ih, register_reciever_wants (ih));
  return l;
}


/**
 * Register SOCKS5 handshake sender
 *
 * @param ih handshake
 * @return non-NULL if the notify callback was queued,
 *         NULL if we are already going to notify someone else (busy)
 */
struct GNUNET_CONNECTION_TransmitHandle *
register_sender (struct GNUNET_SOCKS_Handshake *ih)
{
  struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_MINUTES;

  GNUNET_assert (SOCKS5_step_done > ih->step);
  GNUNET_assert (ih->step >= 0);
  if (0 == ih->step)
    timeout = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 3);
  unsigned char *b = ih->outstep[ih->step];
  unsigned char *e = ih->outstep[ih->step + 1];
  GNUNET_assert (ih->outbuf <= b && b < e && e < &ih->outbuf[1024]);
  ih->th = GNUNET_CONNECTION_notify_transmit_ready (ih->socks5_connection,
                                                    e - b,
                                                    timeout,
                                                    &transmit_ready,
                                                    ih);
  return ih->th;
}


/**
 * Initialize a SOCKS5 handshake for authentication via username and
 * password.  Tor uses SOCKS username and password authentication to assign
 * programs unique circuits.
 *
 * @param user username for the proxy
 * @param pass password for the proxy
 * @return Valid SOCKS5 hanbdshake handle
 */
struct GNUNET_SOCKS_Handshake *
GNUNET_SOCKS_init_handshake (const char *user, const char *pass)
{
  struct GNUNET_SOCKS_Handshake *ih =
    GNUNET_new (struct GNUNET_SOCKS_Handshake);
  unsigned char *b = ih->outbuf;

  ih->outstep[SOCKS5_step_greet] = b;
  *(b++) = 5; /* SOCKS5 */
  unsigned char *n = b++;
  *n = 1; /* Number of authentication methods */
  /* We support no authentication even when requesting authentication,
   * but this appears harmless, given the way that Tor uses authentication.
   * And some SOCKS5 servers might require this.  */
  *(b++) = SOCKS5_AUTH_NOAUTH;
  if (NULL != user)
  {
    *(b++) = SOCKS5_AUTH_USERPASS;
    (*n)++;
  }
  /* There is no apperent reason to support authentication methods beyond
   * username and password since afaik Tor does not support them. */

  /* We authenticate with an empty username and password if the server demands
   * them but we do not have any. */
  if (user == NULL)
    user = "";
  if (pass == NULL)
    pass = "";

  ih->outstep[SOCKS5_step_auth] = b;
  *(b++) = 1; /* subnegotiation ver.: 1 */
  b = SOCK5_proto_string (b, user);
  b = SOCK5_proto_string (b, pass);

  ih->outstep[SOCKS5_step_cmd] = b;

  ih->inend = ih->instart = ih->inbuf;

  return ih;
}


/**
 * Initialize a SOCKS5 handshake without authentication, thereby possibly
 * sharing a Tor circuit with another process.
 *
 * @return Valid SOCKS5 hanbdshake handle
 */
struct GNUNET_SOCKS_Handshake *
GNUNET_SOCKS_init_handshake_noauth ()
{
  return GNUNET_SOCKS_init_handshake (NULL, NULL);
}


/**
 * Build request that the SOCKS5 proxy open a TCP/IP stream to the given host
 * and port.
 *
 * @param ih SOCKS5 handshake
 * @param hostname
 * @param port
 */
void
GNUNET_SOCKS_set_handshake_destination (struct GNUNET_SOCKS_Handshake *ih,
                                        const char *host,
                                        uint16_t port)
{
  union
  {
    struct in_addr in4;
    struct in6_addr in6;
  } ia;
  unsigned char *b = ih->outstep[SOCKS5_step_cmd];

  *(b++) = 5; /* SOCKS5 */
  *(b++) = 1; /* Establish a TCP/IP stream */
  *(b++) = 0; /* reserved */

  /* Specify destination */
  if (1 == inet_pton (AF_INET, host, &ia.in4))
  {
    *(b++) = 1;   /* IPv4 */
    GNUNET_memcpy (b, &ia.in4, sizeof(struct in_addr));
    b += sizeof(struct in_addr);   /* 4 */
  }
  else if (1 == inet_pton (AF_INET6, host, &ia.in6))
  {
    *(b++) = 4;   /* IPv6 */
    GNUNET_memcpy (b, &ia.in6, sizeof(struct in6_addr));
    b += sizeof(struct in6_addr);   /* 16 */
  }
  else
  {
    *(b++) = 3;   /* hostname */
    b = SOCK5_proto_string (b, host);
  }

  /* Specify port */
  *(uint16_t *) b = htons (port);
  b += 2;

  ih->outstep[SOCKS5_step_done] = b;
}


/**
 * Run a SOCKS5 handshake on an open but unused TCP connection.
 *
 * @param ih SOCKS5 handshake, consumed here.
 * @param c open unused connection, consumed here.
 * @return Connection handle that becomes usable when the SOCKS5 handshake completes.
 */
struct GNUNET_CONNECTION_Handle *
GNUNET_SOCKS_run_handshake (struct GNUNET_SOCKS_Handshake *ih,
                            struct GNUNET_CONNECTION_Handle *c)
{
  ih->socks5_connection = c;
  ih->target_connection = GNUNET_CONNECTION_create_proxied_from_handshake (c);
  register_sender (ih);

  return ih->target_connection;
}


/**
 * Check if a SOCKS proxy is required by a service.  Do not use local service
 * if a SOCKS proxy port is configured as this could deanonymize a user.
 *
 * @param service_name name of service to connect to
 * @param cfg configuration to use
 * @return GNUNET_YES if so, GNUNET_NO if not
 */
int
GNUNET_SOCKS_check_service (const char *service_name,
                            const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  return GNUNET_CONFIGURATION_have_value (cfg, service_name, "SOCKSPORT") ||
         GNUNET_CONFIGURATION_have_value (cfg, service_name, "SOCKSHOST");
}


/**
 * Try to connect to a service configured to use a SOCKS5 proxy.
 *
 * @param service_name name of service to connect to
 * @param cfg configuration to use
 * @return Connection handle that becomes usable when the handshake completes.
 *         NULL if SOCKS not configured or not configured properly
 */
struct GNUNET_CONNECTION_Handle *
GNUNET_SOCKS_do_connect (const char *service_name,
                         const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_SOCKS_Handshake *ih;
  struct GNUNET_CONNECTION_Handle *socks5; /* *proxied */
  char *host0;
  char *host1;
  char *user;
  char *pass;
  unsigned long long port0;
  unsigned long long port1;

  if (GNUNET_YES != GNUNET_SOCKS_check_service (service_name, cfg))
    return NULL;
  if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_number (cfg,
                                                          service_name,
                                                          "SOCKSPORT",
                                                          &port0))
    port0 = 9050;
  /* A typical Tor client should usually try port 9150 for the TBB too, but
   * GNUnet can probably assume a system Tor installation. */
  if ((port0 > 65535) || (port0 <= 0))
  {
    LOG (GNUNET_ERROR_TYPE_WARNING,
         _ (
           "Attempting to use invalid port %d as SOCKS proxy for service `%s'.\n"),
         port0,
         service_name);
    return NULL;
  }
  if ((GNUNET_OK != GNUNET_CONFIGURATION_get_value_number (cfg,
                                                           service_name,
                                                           "PORT",
                                                           &port1)) ||
      (port1 > 65535) || (port1 <= 0) ||
      (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg,
                                                           service_name,
                                                           "HOSTNAME",
                                                           &host1)))
  {
    LOG (GNUNET_ERROR_TYPE_WARNING,
         _ (
           "Attempting to proxy service `%s' to invalid port %d or hostname.\n"),
         service_name,
         port1);
    return NULL;
  }
  /* Appeared to still work after host0 corrupted, so either test case is broken, or
     this whole routine is not being called. */
  if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg,
                                                          service_name,
                                                          "SOCKSHOST",
                                                          &host0))
    host0 = NULL;
  socks5 = GNUNET_CONNECTION_create_from_connect (cfg,
                                                  (host0 != NULL) ? host0
                                                  : "127.0.0.1",
                                                  port0);
  GNUNET_free (host0);

  /* Sets to NULL if they do not exist */
  (void) GNUNET_CONFIGURATION_get_value_string (cfg,
                                                service_name,
                                                "SOCKSUSER",
                                                &user);
  (void) GNUNET_CONFIGURATION_get_value_string (cfg,
                                                service_name,
                                                "SOCKSPASS",
                                                &pass);
  ih = GNUNET_SOCKS_init_handshake (user, pass);
  GNUNET_free (user);
  GNUNET_free (pass);

  GNUNET_SOCKS_set_handshake_destination (ih, host1, port1);
  GNUNET_free (host1);
  return GNUNET_SOCKS_run_handshake (ih, socks5);
}


/* socks.c */
