/*
 * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2005, 2008, 2009, 2011, 2012,
 *               2013
 *      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"

static const char rcsid[] =
"$Id: authneg.c,v 1.128 2013/10/27 15:24:42 karls Exp $";

int
negotiate_method(s, packet, route, emsg, emsglen)
   int s;
   socks_t *packet;
   route_t *route;
   char *emsg;
   const size_t emsglen;
{
   const char *function = "negotiate_method()";
   ssize_t rc;
   size_t i, requestlen;
   unsigned char *name = NULL, *password = NULL;
   unsigned char request[ 1                  /* version                       */
                        + 1                  /* number of methods to offer.   */
                        + METHODS_KNOWN      /* methods we offer server.      */
                        ];
   unsigned char response[ 1   /* version.                     */
                         + 1   /* method selected by server.   */
                         ];
   char buf[256], lemsg[512] /* local emesg. */;
   int intmethodv[METHODS_KNOWN];

   if (sockscf.option.debug)
      slog(LOG_DEBUG, "%s: fd %d, %s", function, s, socket2string(s, NULL, 0));

#if !SOCKS_CLIENT && HAVE_GSSAPI
   switch (packet->req.auth->method) {
      case AUTHMETHOD_GSSAPI:
         /*
          * Nothing from gssapistate with client we are currently
          * offering upstream proxyserver, so reset authmethod to none
          * so as to not confuse things and make any part of the code
          * try to think this gssapi state relates to the upstream
          * proxy.
          */
         bzero(&packet->req.auth->mdata.gssapi,
               sizeof(packet->req.auth->mdata.gssapi));

         packet->req.auth->method = AUTHMETHOD_NOTSET;
         break;
   }
#endif /* !SOCKS_CLIENT && HAVE_GSSAPI */


   if (packet->req.version == PROXY_SOCKS_V4) {
      slog(LOG_DEBUG,
           "%s: no method negotiate in %s.  Setting authmethod to %s",
           function,
           proxyprotocol2string(packet->req.version),
           method2string(AUTHMETHOD_NONE));


      packet->req.auth->method = AUTHMETHOD_NONE;
      packet->res.auth->method = AUTHMETHOD_NONE;

      return 0;
   }

   SASSERTX(packet->gw.state.smethodc > 0);
   SASSERTX(packet->gw.state.smethodc <= METHODS_KNOWN);

   /*
    * create request packet.
    * version numberOfmethods methods ...
    */

   requestlen            = 0;
   request[requestlen++] = packet->req.version;

   SASSERTX(requestlen == AUTH_NMETHODS);
   request[requestlen++] = (unsigned char)0;
   SASSERTX(request[AUTH_NMETHODS] == 0);

   /*
    * Count and add the methods we support and are configured to offer
    * this server.
    */
   for (i = 0; i < packet->gw.state.smethodc; ++i) {
      if (packet->req.auth->method != AUTHMETHOD_NOTSET) {
         /*
          * Must be doing serverchaining.  Not all methods we are
          * configured to support for this route may be supported
          * for this particular client.  E.g., if the client has
          * not provided us with a username/password, we can not
          * provide the upstream proxy with it either, so don't
          * offer it.
          */

         SASSERTX(!SOCKS_CLIENT);

         switch (packet->gw.state.smethodv[i]) {
            case AUTHMETHOD_NONE:
               break; /* ok. */

            case AUTHMETHOD_UNAME:
               if (packet->req.auth->method != AUTHMETHOD_UNAME)
                  continue; /* don't offer this method. */

               break;

            case AUTHMETHOD_GSSAPI:
               break; /*
                       * ok?  Can't forward gssapi/kerberos credentials,
                       * but operator should be able to set up a
                       * things so we can initiate our own gssapi
                       * session to the upsteam proxy.
                       */

            default:
               SERRX(packet->gw.state.smethodv[i]);
         }
      }

      request[requestlen++] = (unsigned char)packet->gw.state.smethodv[i];
      ++request[AUTH_NMETHODS];
   }

   SASSERTX(request[AUTH_NMETHODS] > 0);
   SASSERTX(request[AUTH_NMETHODS] <= METHODS_KNOWN);
   SASSERTX(request[AUTH_NMETHODS] <= ELEMENTS(intmethodv));

   charmethod2intmethod((ssize_t)request[AUTH_NMETHODS],
                        &request[AUTH_FIRSTMETHOD],
                        intmethodv);

   slog(LOG_NEGOTIATE, "%s: offering proxy server #%d method%s: %s",
        function,
        request[AUTH_NMETHODS],
        request[AUTH_NMETHODS] == 1 ? "" : "s",
        methods2string(request[AUTH_NMETHODS], intmethodv, buf, sizeof(buf)));

   if (socks_sendton(s,
                     request,
                     requestlen,
                     requestlen,
                     0,
                     NULL,
                     0,
                     NULL,
                     NULL) != (ssize_t)requestlen) {
      snprintf(emsg, emsglen,
               "could not offer list of auth methods to proxy server: "
               "send failed: %s",
               strerror(errno));

      socks_blacklist(route, emsg);
      return -1;
   }

   if ((rc = socks_recvfromn(s,
                             response,
                             sizeof(response),
                             sizeof(response),
                             0,
                             NULL,
                             NULL,
                             NULL,
                             NULL)) != sizeof(response)) {
      snprintf(emsg, emsglen,
               "could not read proxy server's response concerning method to "
               "use, read %ld/%lu: %s",
               (long)rc,
               (unsigned long)sizeof(response),
               rc == 0 ? "server closed connection" : strerror(errno));

      socks_blacklist(route, emsg);
      return -1;
   }

   /*
    * sanity check server's reply.
    */

   SASSERTX(AUTH_VERSION <= rc);
   if (request[AUTH_VERSION] != response[AUTH_VERSION]) {
      snprintf(emsg, emsglen,
               "got reply version %d from proxy server, but expected version "
               "%d.  Remote proxy server problem?",
               response[AUTH_VERSION], request[AUTH_VERSION]);

      socks_blacklist(route, emsg);
      return -1;
   }
   packet->version = request[AUTH_VERSION];

   SASSERTX(AUTH_SELECTEDMETHOD <= rc);
   if (!methodisset(response[AUTH_SELECTEDMETHOD],
                    intmethodv,
                    request[AUTH_NMETHODS])) {
      if (response[AUTH_SELECTEDMETHOD] == AUTHMETHOD_NOACCEPT)
         snprintf(emsg, emsglen,
                  "proxy server said we did not offer any acceptable "
                  "authentication methods");
      else {
         snprintf(emsg, emsglen,
                  "proxy server selected method 0x%x (%s), but that is not "
                  "among the methods we offered it",
                  response[AUTH_SELECTEDMETHOD],
                  method2string(response[AUTH_SELECTEDMETHOD]));

         swarnx("%s: %s", function, emsg);
      }

      socks_blacklist(route, emsg);
      return -1;
   }

   slog(LOG_NEGOTIATE, "%s: proxy server selected method %s",
        function, method2string(response[AUTH_SELECTEDMETHOD]));

   switch (response[AUTH_SELECTEDMETHOD]) {
      case AUTHMETHOD_NONE:
         rc = 0;
         break;

#if HAVE_GSSAPI
      case AUTHMETHOD_GSSAPI:
         if (clientmethod_gssapi(s,
                                 packet->req.protocol,
                                 &packet->gw,
                                 packet->req.version,
                                 packet->req.auth,
                                 lemsg,
                                 sizeof(lemsg)) == 0)
            rc = 0;
         else
            rc = -1;
         break;
#endif /* HAVE_GSSAPI */

      case AUTHMETHOD_UNAME:
         if (clientmethod_uname(s,
                                &packet->gw.addr,
                                packet->req.version,
                                name,
                                password,
                                lemsg,
                                sizeof(lemsg)) == 0)
            rc = 0;
         else
            rc = -1;
         break;

      case AUTHMETHOD_NOACCEPT:
         snprintf(lemsg, sizeof(lemsg),
                  "proxy server did not accept any of the authentication "
                  "methods we offered it");

         socks_blacklist(route, emsg);
         rc = -1;
         break;

      default:
         SERRX(packet->req.auth->method);
   }

   packet->req.auth->method = response[AUTH_SELECTEDMETHOD];

   if (rc == 0) {
      slog(LOG_NEGOTIATE, "%s: established v%d connection using method %d",
           function, packet->version, packet->req.auth->method);

      errno = 0; /* all is ok. */
   }
   else {
      snprintf(emsg, emsglen,
               "failed to establish v%d connection using method %d: %s",
               packet->version, packet->req.auth->method, lemsg);

      slog(LOG_DEBUG, "%s: %s", function, emsg);
   }

   return (int)rc;
}
