/*
 * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2005, 2008, 2009, 2010, 2011,
 *               2012, 2013, 2016, 2017
 *      Inferno Nettverk A/S, Norway.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. The above copyright notice, this list of conditions and the following
 *    disclaimer must appear in all copies of the software, derivative works
 *    or modified versions, and any portions thereof, aswell as in all
 *    supporting documentation.
 * 2. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by
 *      Inferno Nettverk A/S, Norway.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Inferno Nettverk A/S requests users of this software to return to
 *
 *  Software Distribution Coordinator  or  sdc@inet.no
 *  Inferno Nettverk A/S
 *  Oslo Research Park
 *  Gaustadalléen 21
 *  NO-0349 Oslo
 *  Norway
 *
 * any improvements or extensions that they make and grant Inferno Nettverk A/S
 * the rights to redistribute these changes.
 *
 */

#include "common.h"

#include "upnp.h"

static const char rcsid[] =
"$Id: Rconnect.c,v 1.242.6.3 2017/01/31 08:17:38 karls Exp $";

int
Rconnect(s, _name, namelen)
   int s;
   const struct sockaddr *_name;
   socklen_t namelen;
{
   const char *function = "Rconnect()";
   const int force_blockingconnect
   = socks_getenv(ENV_SOCKS_FORCE_BLOCKING_CONNECT, istrue) == NULL ? 0 : 1;
   struct sockaddr_storage name;
   socksfd_t socksfd;
   sockshost_t src, dst;
   authmethod_t auth;
   socks_t packet;
   socklen_t len;
   char namestr[MAXSOCKADDRSTRING], emsg[256];
   int type, rc, fd_is_nonblocking, savederrno;

   clientinit();

   if (_name == NULL) {
      rc = connect(s, _name, namelen);

      slog(LOG_DEBUG,
           "%s: dst is NULL, fallback to system connect(2) returned %d",
           function, rc);

      return rc;
   }

   if (_name->sa_family != AF_INET) {
      rc = connect(s, _name, namelen);

      slog(LOG_DEBUG,
           "%s: unsupported address family '%d' for dst %s, "
           "fallback to system connect(2) returned %d",
           function,
           _name->sa_family,
           sockaddr2string(TOCSS(_name), NULL, 0),
           rc);

      return rc;
   }

   usrsockaddrcpy(&name, TOCSS(_name), MIN(sizeof(name), namelen));

   if (socks_socketisforlan(s)) {
      slog(LOG_DEBUG, "%s: fd %d wanting to connect to %s is a lan-only "
                      "socket.  Falling back to system connect",
                      function,
                      s,
                      sockaddr2string(&name, namestr, sizeof(namestr)));

      return connect(s, _name, namelen);
   }


   slog(LOG_INFO, "%s: fd %d, address %s",
        function,
        s,
        sockaddr2string(&name, namestr, sizeof(namestr)));

   if (socks_addrisours(s, &socksfd, 1)) {
      slog(LOG_DEBUG, "%s: socket is a %s socket, err = %d, inprogress = %d",
                      function, proxyprotocol2string(socksfd.state.version),
                      socksfd.state.err, socksfd.state.inprogress);

      switch (socksfd.state.command) {
         case SOCKS_BIND:
            if (socksfd.state.protocol.tcp) {
               /*
                * Our guess; the client has succeeded to bind a specific
                * address and is now trying to connect out from it.
                * That also indicates the socks server is listening on a port
                * for this client.
                * Can't accept() on a connected socket so lets close the
                * connection to the server so it can stop listening on our
                * behalf, and we continue as if this was an ordinary connect().
                * Can only hope the server will use same port as we for
                * connecting out.
                *
                * Client might get problems if it has done a getsockname(2)
                * already, and thus thinks it knows it's local address,
                * as this Rconnect() will have to change it.
                */
               int tmp_s;

               slog(LOG_DEBUG,
                    "%s: continuing with Rconnect() after Rbind() on fd %d",
                    function, s);

               if (socksfd.state.version == PROXY_UPNP)
                  upnpcleanup(s);
               else {
                  /*
                   * socket must have connected to proxy before for Rbind().
                   * Need a new one.
                   */

                  if ((tmp_s = socketoptdup(s, -1)) == -1)
                     break;

                  if (dup2(tmp_s, s) == -1) {
                     close(tmp_s);
                     break;
                  }

                  close(tmp_s);
                  socks_rmaddr(s, 1);
               }
            }
            else if (socksfd.state.protocol.udp) {
               /*
                * Previously bound the udp socket, and now want to
                * connect out on the same socket.  In this case
                * we want to keep the port bound on the server, and
                * just add a connect to the peer, so let udpsetup() do
                * it's thing.
                */
            }
            else
               SERRX(0);

            break;

         case SOCKS_CONNECT:
            if (socksfd.state.version == PROXY_UPNP) {
               rc = connect(s, TOSA(&name), namelen);

               slog(LOG_DEBUG, "%s: connect(2) called again on upnp socket "
                               "returned %d, errno = %d (%s)",
                               function, rc, errno, strerror(errno));

               return rc;
            }

            if (socksfd.state.err != 0)
               errno = socksfd.state.err;
            else {
               if (socksfd.state.inprogress)
                  errno = EALREADY;
               else
                  errno = EISCONN;
            }

            return -1;

         case SOCKS_UDPASSOCIATE:
            /*
             * Trying to connect a udp socket (to a new address)?
             * Just continue as usual, udpsetup() will reuse existing
             * setup and we just assign the new ("connected") address.
             */
            break;

         default:
            SERRX(socksfd.state.command);
      }
   }
   else {
      slog(LOG_DEBUG,
           "%s: unknown fd %d.  Doing socks_rmaddr() before continuing ...",
           function, s);

      socks_rmaddr(s, 1);
   }

   len = sizeof(type);
   if (getsockopt(s, SOL_SOCKET, SO_TYPE, &type, &len) != 0) {
      swarn("%s: getsockopt(SO_TYPE)", function);
      return -1;
   }

   bzero(&packet, sizeof(packet));

   switch (type) {
      case SOCK_DGRAM:
         socksfd.route = udpsetup(s, &name, SOCKS_SEND, 1, emsg, sizeof(emsg));
         if (socksfd.route == NULL) {
            slog(LOG_INFO,
                 "%s: udpsetup() returned no route to %s for fd %d: %s",
                 function,
                 sockaddr2string(&name, NULL, 0),
                 s,
                 emsg);

            errno = ENETUNREACH;
            return -1;
         }

         slog(LOG_INFO, "%s: route set up for fd %d to %s is a %s route",
              function,
              s,
              sockaddr2string(&name, NULL, 0),
              proxyprotocols2string(&socksfd.route->gw.state.proxyprotocol,
                                    NULL,
                                    0));

          if (socksfd.route->gw.state.proxyprotocol.direct) {
             const ssize_t rc = connect(s, TOSA(&name), namelen);

             slog(LOG_DEBUG,
                  "%s: direct connect on fd %d to %s returned %ld: (%s)",
                  function,
                  s,
                  sockaddr2string(&name, NULL, 0),
                  (long)rc,
                  strerror(errno));

             return (int)rc;
          }
          else
             return 0;

      case SOCK_STREAM:
         packet.req.protocol = SOCKS_TCP;
         break;

      default:
         swarnx("%s: unknown protocol type %d, falling back to system connect",
                function, type);
         return connect(s, TOSA(&name), namelen);
   }

   bzero(&socksfd, sizeof(socksfd));
   len = sizeof(socksfd.local);
   if (getsockname(s, TOSA(&socksfd.local), &len) != 0)
      return -1;

   slog(LOG_DEBUG, "%s: local address of fd %d is %s",
        function, s, sockaddr2string(&socksfd.local, NULL, 0));

   bzero(&src, sizeof(src)); /* silence valgrind warning */
   src.atype     = SOCKS_ADDR_IPV4;
   src.addr.ipv4 = TOIN(&socksfd.local)->sin_addr;
   src.port      = TOIN(&socksfd.local)->sin_port;

   bzero(&dst, sizeof(dst)); /* silence valgrind warning */
   fakesockaddr2sockshost(&name, &dst);

   bzero(&auth, sizeof(auth));
   auth.method        = AUTHMETHOD_NOTSET;

   packet.req.version = PROXY_DIRECT;
   packet.req.command = SOCKS_CONNECT;
   packet.req.host    = dst;
   packet.req.auth    = &auth;

   if ((socksfd.route = socks_requestpolish(&packet.req, &src, &dst)) == NULL)
      return -1;

   if (socksfd.route->gw.state.proxyprotocol.direct)
      return connect(s, _name, namelen);

   if (socks_routesetup(s, s, socksfd.route, emsg, sizeof(emsg)) != 0) {
      swarnx("%s: socks_routesetup() failed: %s", function, emsg);
      return -1;
   }

   /* again in case routesetup() changed local address. */
   len = sizeof(socksfd.local);
   if (getsockname(s, TOSA(&socksfd.local), &len) != 0)
      return -1;

   slog(LOG_DEBUG, "%s: local address of fd %d is %s",
        function, s, sockaddr2string(&socksfd.local, NULL, 0));

   src.addr.ipv4 = TOIN(&socksfd.local)->sin_addr;
   src.port      = TOIN(&socksfd.local)->sin_port;

   slog(LOG_DEBUG, "%s: route prepared for fd %d is a %s route",
        function, s, proxyprotocol2string(packet.req.version));

   if (packet.req.version == PROXY_DIRECT) {
      const int rc = connect(s, TOSA(&name), namelen);

      slog(LOG_DEBUG, "%s: direct connect on fd %d to %s returned %d: (%s)",
           function,
           s,
           sockaddr2string(&name, NULL, 0),
           rc,
           strerror(errno));

      return rc;
   }

   switch (packet.version = packet.req.version) {
      case PROXY_SOCKS_V4:
      case PROXY_SOCKS_V5:
      case PROXY_HTTP_10:
      case PROXY_HTTP_11:
      case PROXY_UPNP:
         socksfd.control = s;
         break;

      default:
         SERRX(packet.req.version);
   }

   if (packet.version == PROXY_UPNP)
      /*
       * no negotiation to do before the connect, so we don't need
       * to care here whether the socket is blocking or not.
       */
      fd_is_nonblocking = 0;
   else
      /*
       * Check if the socket is non-blocking.  If so, fork a child
       * to negotiate with the proxy server and establish the connection.
       * In the case of UPNP, no negotiation is done, so don't waste
       * time on that.
       */
      fd_is_nonblocking = !fdisblocking(s);

   errno = 0;
   if (fd_is_nonblocking && !force_blockingconnect) {
      socksfd.route = socks_nbconnectroute(s,
                                           socksfd.control,
                                           &packet,
                                           &src,
                                           &dst,
                                           emsg,
                                           sizeof(emsg));

      if (socksfd.route == NULL)
         SASSERTX(errno != 0);
   }
   else
      socksfd.route = socks_connectroute(socksfd.control,
                                         &packet,
                                         &src,
                                         &dst,
                                         emsg,
                                         sizeof(emsg));

   slog(LOG_INFO, "%s: %s a %s route for fd %d, errno = %d",
        function,
        socksfd.route == NULL ?  "could not establish" : "established",
        proxyprotocol2string(packet.req.version),
        s,
        errno);

   if (socksfd.route == NULL) {
      if (s != socksfd.control)
         close(socksfd.control);

      switch (errno) {
         case EADDRINUSE: {
            /*
             * This problem can arise when we are socksifying
             * a server application that does several outbound
             * connections from the same address (e.g. ftpd) to the
             * same socks server.
             * It has by now successfully bound the address (it thinks)
             * and is not expecting this error.
             * Not sure what is best to do, just failing here prevents
             * ftpd from working for clients using the PORT command.
             *
             * For now, lets retry with a new socket.
             * This means the server no longer has bound the address
             * it (may) think it has of course, so not sure how smart this
             * really is.
             */
            int tmp_s;

            swarn("%s: server socksified?  trying to work around problem...",
                  function);

            if ((tmp_s = socketoptdup(s, -1)) == -1)
               break;
            if (dup2(tmp_s, s) == -1)
               break;
            close(tmp_s);

            /*
             * if s was bound to a privileged port, try to bind the new
             * s too to a privileged port.
             */
            /* LINTED pointer casts may be troublesome */
            if (PORTISRESERVED(TOIN(&socksfd.local)->sin_port)) {
               /* LINTED pointer casts may be troublesome */
               TOIN(&socksfd.local)->sin_port = htons(0);

               /* LINTED pointer casts may be troublesome */
               bindresvport(s, TOIN(&socksfd.local));
            }

            return Rconnect(s, TOSA(&name), namelen);
         }
      }

      swarnx("%s: could not connect route: %s", function, emsg);
      return -1;
   }

   if (fd_is_nonblocking && !force_blockingconnect) {
      if (!socks_addrisours(s, &socksfd, 1)) {
         swarn("%s: something went wrong when setting up non-blocking connect",
                function);

         return -1;
      }

      if (socksfd.state.inprogress) {
         slog(LOG_DEBUG, "%s: got route, non-blocking connect in progress",
                          function);

         errno = EINPROGRESS;
         return -1;
      }
      else {
         if (socksfd.state.err != 0) {
            slog(LOG_INFO,
                 "%s: non-blocking connect to %s failed with errno %d (%s)",
                 function,
                 sockaddr2string(&name, NULL, 0),
                 socksfd.state.err,
                 strerror(socksfd.state.err));

            errno = socksfd.state.err;
            return -1;
         }
         else {
            slog(LOG_INFO,
                 "%s: got route, non-blocking connect to %s finished ok",
                 function, sockaddr2string(&name, NULL, 0));

            return 0;
         }
      }
   }

   if (fd_is_nonblocking && force_blockingconnect)
      setblocking(s, "Rconnect(): forcing connect(2) to block");

   rc = socks_negotiate(s,
                        socksfd.control,
                        &packet,
                        socksfd.route,
                        emsg,
                        sizeof(emsg));

   if (fd_is_nonblocking && force_blockingconnect)
      setnonblocking(s, "Rconnect()");

   if (rc != 0) {
      swarnx("%s: socks_negotiate() failed: %s", function, emsg);

      return -1;
   }

   savederrno = errno;

   update_after_negotiate(&packet, &socksfd);

   slog(LOG_DEBUG,
        "%s: errno after successful socks_negotiate() is %d, version is %d",
        function, savederrno, socksfd.state.version);

   SASSERTX(type == SOCK_STREAM);
   socksfd.state.protocol.tcp    = 1;

   sockshost2sockaddr(&packet.res.host, &socksfd.remote);
   sockaddr2sockshost(&name, &socksfd.forus.connected);

   /* LINTED pointer casts may be troublesome */
   if (TOIN(&socksfd.local)->sin_port != htons(0)
   &&  TOIN(&socksfd.local)->sin_port != TOIN(&socksfd.remote)->sin_port) {
      /*
       * unfortunate; the client is trying to connect from a specific
       * port, a port it has successfully bound, but the port is currently
       * in use on the server side or the server doesn't care.
       */

      /* LINTED pointer casts may be troublesome */
      slog(LOG_DEBUG,
           "%s: wanted port %u, but got %u.  Continuing anyway",
           function,
           ntohs(TOIN(&socksfd.local)->sin_port),
           ntohs(TOIN(&socksfd.remote)->sin_port));
   }

   len = sizeof(socksfd.server);
   if (getpeername(s, TOSA(&socksfd.server), &len) == 0)
      slog(LOG_DEBUG, "%s: getpeername(fd %d) = %s",
           function, s, sockaddr2string(&socksfd.server, NULL, 0));
   else
      slog(LOG_DEBUG, "%s: getpeername(fd %d): %s",
           function, s, strerror(errno));

   len = sizeof(socksfd.local);
   if (getsockname(s, TOSA(&socksfd.local), &len) != 0)
      slog(LOG_DEBUG, "%s: getsockname(s): %s", function, strerror(errno));

   socks_addaddr(s, &socksfd, 1);

   sockaddr2sockshost(&name, &sockscf.state.lastconnect); /* for socks bind. */

   slog(LOG_DEBUG, "%s: returning.  Current errno is %d (%s), saved is %d",
        function, errno, strerror(errno), savederrno);

   errno = savederrno;

   if (errno != 0)
      return -1;

   return 0;
}
