/*
 * This file is part of the Nice GLib ICE library.
 *
 * (C) 2008-2009 Collabora Ltd.
 *  Contact: Youness Alaoui
 * (C) 2007-2009 Nokia Corporation. All rights reserved.
 *  Contact: Rémi Denis-Courmont
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the Nice GLib ICE library.
 *
 * The Initial Developers of the Original Code are Collabora Ltd and Nokia
 * Corporation. All Rights Reserved.
 *
 * Contributors:
 *   Youness Alaoui, Collabora Ltd.
 *   Rémi Denis-Courmont, Nokia
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * the GNU Lesser General Public License Version 2.1 (the "LGPL"), in which
 * case the provisions of LGPL are applicable instead of those above. If you
 * wish to allow use of your version of this file only under the terms of the
 * LGPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replace
 * them with the notice and other provisions required by the LGPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the LGPL.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include "win32_common.h"
#define close closesocket
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <errno.h>
#include <sys/time.h>
#include <fcntl.h>
#endif


#ifdef HAVE_POLL
# include <poll.h>
#endif


#include "bind.h"
#include "stun/stunagent.h"

#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include "timer.h"


#ifndef SOL_IP
# define SOL_IP IPPROTO_IP
#endif

#ifndef SOL_IPV6
# define SOL_IPV6 IPPROTO_IPV6
#endif


/** Non-blocking mode STUN binding discovery */

size_t stun_usage_bind_create (StunAgent *agent, StunMessage *msg,
    uint8_t *buffer, size_t buffer_len)
{
  stun_agent_init_request (agent, msg, buffer, buffer_len, STUN_BINDING);

  return stun_agent_finish_message (agent, msg, NULL, 0);
}

StunUsageBindReturn stun_usage_bind_process (StunMessage *msg,
    struct sockaddr *addr, socklen_t *addrlen,
    struct sockaddr *alternate_server, socklen_t *alternate_server_len)
{
  int code = -1;
  StunMessageReturn val;
  union {
    struct sockaddr *sa;
    struct sockaddr_storage *sas;
  } sa;

  sa.sa = addr;

  if (stun_message_get_method (msg) != STUN_BINDING)
    return STUN_USAGE_BIND_RETURN_INVALID;

  switch (stun_message_get_class (msg))
  {
    case STUN_REQUEST:
    case STUN_INDICATION:
      return STUN_USAGE_BIND_RETURN_INVALID;

    case STUN_RESPONSE:
      break;

    case STUN_ERROR:
      if (stun_message_find_error (msg, &code) != STUN_MESSAGE_RETURN_SUCCESS) {
        /* missing ERROR-CODE: ignore message */
        return STUN_USAGE_BIND_RETURN_INVALID;
      }

      /* NOTE: currently we ignore unauthenticated messages if the context
       * is authenticated, for security reasons. */
      stun_debug (" STUN error message received (code: %d)", code);

      /* ALTERNATE-SERVER mechanism */
      if ((code / 100) == 3) {
        union {
          struct sockaddr *sa;
          struct sockaddr_storage *sas;
        } alternate_sa;

        alternate_sa.sa = alternate_server;
        if (alternate_server && alternate_server_len) {
          if (stun_message_find_addr (msg, STUN_ATTRIBUTE_ALTERNATE_SERVER,
                  alternate_sa.sas,
                  alternate_server_len) != STUN_MESSAGE_RETURN_SUCCESS) {
            stun_debug (" Unexpectedly missing ALTERNATE-SERVER attribute");
            return STUN_USAGE_BIND_RETURN_ERROR;
          }
        } else {
          if (!stun_message_has_attribute (msg, STUN_ATTRIBUTE_ALTERNATE_SERVER)) {
            stun_debug (" Unexpectedly missing ALTERNATE-SERVER attribute");
            return STUN_USAGE_BIND_RETURN_ERROR;
          }
        }

        stun_debug ("Found alternate server");
        return STUN_USAGE_BIND_RETURN_ALTERNATE_SERVER;

      }
      return STUN_USAGE_BIND_RETURN_ERROR;

    default:
      /* Fall through. */
      break;
  }

  stun_debug ("Received %u-bytes STUN message", stun_message_length (msg));

  val = stun_message_find_xor_addr (msg,
      STUN_ATTRIBUTE_XOR_MAPPED_ADDRESS, sa.sas,
      addrlen);
  if (val != STUN_MESSAGE_RETURN_SUCCESS)
  {
    stun_debug (" No XOR-MAPPED-ADDRESS: %d", val);
    val = stun_message_find_addr (msg,
        STUN_ATTRIBUTE_MAPPED_ADDRESS, sa.sas,
        addrlen);
    if (val != STUN_MESSAGE_RETURN_SUCCESS)
    {
      stun_debug (" No MAPPED-ADDRESS: %d", val);
      return STUN_USAGE_BIND_RETURN_ERROR;
    }
  }

  stun_debug (" Mapped address found!");
  return STUN_USAGE_BIND_RETURN_SUCCESS;

}


/** Binding keep-alive (Binding discovery indication!) */

size_t
stun_usage_bind_keepalive (StunAgent *agent, StunMessage *msg,
    uint8_t *buf, size_t len)
{

  stun_agent_init_indication (agent, msg,
      buf, len, STUN_BINDING);
  return stun_agent_finish_message (agent, msg, NULL, 0);
}



typedef struct stun_trans_s
{

  int fd;
  int own_fd;
  socklen_t dstlen;
  struct sockaddr_storage dst;
} StunTransport;


typedef enum {
  STUN_USAGE_TRANS_RETURN_SUCCESS,
  STUN_USAGE_TRANS_RETURN_ERROR,
  STUN_USAGE_TRANS_RETURN_RETRY,
  STUN_USAGE_TRANS_RETURN_INVALID_ADDRESS,
  STUN_USAGE_TRANS_RETURN_UNSUPPORTED,
} StunUsageTransReturn;




static StunUsageTransReturn
stun_trans_init (StunTransport *tr, int fd,
    const struct sockaddr *srv, socklen_t srvlen)
{
  assert (fd != -1);

  if ((size_t) srvlen > sizeof (tr->dst))
    return STUN_USAGE_TRANS_RETURN_INVALID_ADDRESS;

  tr->own_fd = -1;
  tr->fd = fd;

  tr->dstlen = srvlen;
  memcpy (&tr->dst, srv, srvlen);

  return STUN_USAGE_TRANS_RETURN_SUCCESS;
}


/*
 * Creates and connects a socket. This is useful when a socket is to be used
 * for multiple consecutive transactions (e.g. TURN).
 */
static int stun_socket (int family, int type, int proto)
{
#ifdef _WIN32
  unsigned long set_nonblock=1;
#endif

  int fd = socket (family, type, proto);
  if (fd == -1)
    return -1;

#ifdef FD_CLOEXEC
  fcntl (fd, F_SETFD, fcntl (fd, F_GETFD) | FD_CLOEXEC);
#endif
#ifdef O_NONBLOCK
  fcntl (fd, F_SETFL, fcntl (fd, F_GETFL) | O_NONBLOCK);
#elif defined _WIN32
  ioctlsocket(fd, FIONBIO, &set_nonblock);
#endif

#ifdef MSG_ERRQUEUE
  if (type == SOCK_DGRAM)
  {
    /* Linux specifics for ICMP errors on non-connected sockets */
    int yes = 1;
    switch (family)
    {
      case AF_INET:
        setsockopt (fd, SOL_IP, IP_RECVERR, &yes, sizeof (yes));
        break;
      case AF_INET6:
        setsockopt (fd, SOL_IPV6, IPV6_RECVERR, &yes, sizeof (yes));
        break;
      default:
        /* Nothing to do. */
        break;
    }
  }
#endif

  return fd;
}


static StunUsageTransReturn
stun_trans_create (StunTransport *tr, int type, int proto,
    const struct sockaddr *srv, socklen_t srvlen)
{
  StunUsageTransReturn val = STUN_USAGE_TRANS_RETURN_ERROR;
  int fd;

  if ((size_t) srvlen < sizeof(*srv))
    return STUN_USAGE_TRANS_RETURN_INVALID_ADDRESS;

  fd = stun_socket (srv->sa_family, type, proto);
  if (fd == -1)
    return STUN_USAGE_TRANS_RETURN_ERROR;

  if (type != SOCK_DGRAM) {
    if (connect (fd, srv, srvlen) &&
#ifdef _WIN32
        (WSAGetLastError () != WSAEINPROGRESS)) {
#else
      (errno != EINPROGRESS)) {
#endif
      goto error;
    }
    val = stun_trans_init (tr, fd, NULL, 0);
  } else {
    val = stun_trans_init (tr, fd, srv, srvlen);
  }

  if (val)
    goto error;

  tr->own_fd = tr->fd;
  return STUN_USAGE_TRANS_RETURN_SUCCESS;

error:
  close (fd);
  return val;
}


static void stun_trans_deinit (StunTransport *tr)
{
  int saved = errno;

  assert (tr->fd != -1);

  if (tr->own_fd != -1)
    close (tr->own_fd);

  tr->own_fd = -1;
  tr->fd = -1;

  errno = saved;
}


#ifndef MSG_DONTWAIT
# define MSG_DONTWAIT 0
#endif
#ifndef MSG_NOSIGNAL
# define MSG_NOSIGNAL 0
#endif


static int stun_err_dequeue (int fd)
{
#ifdef MSG_ERRQUEUE
  struct msghdr hdr;
  int saved_errno = errno, ret;

  memset (&hdr, 0, sizeof (hdr));
  ret = (recvmsg (fd, &hdr, MSG_ERRQUEUE) >= 0);
  errno = saved_errno;
  return ret;
#else
  (void) fd;
  return 0;
#endif
}


static ssize_t
stun_trans_sendto (StunTransport *tr, const uint8_t *buf, size_t len,
    const struct sockaddr *dst, socklen_t dstlen)
{
  static const int flags = MSG_DONTWAIT | MSG_NOSIGNAL;
  ssize_t val;

  do
  {
    if (dstlen > 0)
      val = sendto (tr->fd, (void *)buf, len, flags, dst, dstlen);
    else
      val = send (tr->fd, (void *)buf, len, flags);
  }
  while ((val == -1) && stun_err_dequeue (tr->fd));

  return val;
}


static ssize_t
stun_trans_recvfrom (StunTransport *tr, uint8_t *buf, size_t maxlen,
                       struct sockaddr_storage * dst,
                       socklen_t * dstlen)
{
  static const int flags = MSG_DONTWAIT | MSG_NOSIGNAL;
  ssize_t val;

  if (dstlen != NULL)
    val = recvfrom (tr->fd, (void *)buf, maxlen, flags, (struct sockaddr *) dst,
        dstlen);
  else
    val = recv (tr->fd, (void *)buf, maxlen, flags);

  if (val == -1)
    stun_err_dequeue (tr->fd);

  return val;
}


static ssize_t
stun_trans_send (StunTransport *tr, const uint8_t *buf, size_t len)
{
  struct sockaddr *conv;

  conv = (struct sockaddr *) &tr->dst;

  return stun_trans_sendto (tr, buf, len, conv, tr->dstlen);
}

static ssize_t
stun_trans_recv (StunTransport *tr, uint8_t *buf, size_t maxlen)
{
  return stun_trans_recvfrom (tr, buf, maxlen, NULL, NULL);
}


#ifdef HAVE_POLL
static int stun_trans_fd (const StunTransport *tr)
{
  assert (tr != NULL);
  return tr->fd;
}
#endif


/*
 * Waits for a response or timeout to occur.
 *
 * @return ETIMEDOUT if the transaction has timed out, or 0 if an incoming
 * message needs to be processed.
 */
static StunUsageTransReturn
stun_trans_poll (StunTransport *tr, unsigned int delay)
{
#ifdef HAVE_POLL
  struct pollfd ufd;

  memset (&ufd, 0, sizeof (ufd));
  ufd.fd = stun_trans_fd (tr);

  ufd.events |= POLLIN;

  if (poll (&ufd, 1, delay) <= 0) {
    return STUN_USAGE_TRANS_RETURN_RETRY;
  }

  return STUN_USAGE_TRANS_RETURN_SUCCESS;
#else
  (void)tr;
  return STUN_USAGE_TRANS_RETURN_UNSUPPORTED;
#endif
}



/** Blocking mode STUN binding discovery */
StunUsageBindReturn stun_usage_bind_run (const struct sockaddr *srv,
    socklen_t srvlen, struct sockaddr_storage *addr, socklen_t *addrlen)
{
  StunTimer timer;
  StunTransport trans;
  StunAgent agent;
  StunMessage req;
  uint8_t req_buf[STUN_MAX_MESSAGE_SIZE];
  StunMessage msg;
  uint8_t buf[STUN_MAX_MESSAGE_SIZE];
  StunValidationStatus valid;
  size_t len;
  StunUsageTransReturn ret;
  int val;
  struct sockaddr_storage alternate_server = { AF_UNSPEC } ;
  socklen_t alternate_server_len = sizeof (alternate_server);
  StunUsageBindReturn bind_ret;

  trans.fd = -1;

  stun_agent_init (&agent, STUN_ALL_KNOWN_ATTRIBUTES,
      STUN_COMPATIBILITY_RFC3489, 0);

  len = stun_usage_bind_create (&agent, &req, req_buf, sizeof(req_buf));

  ret = stun_trans_create (&trans, SOCK_DGRAM, 0, srv, srvlen);
  if (ret != STUN_USAGE_TRANS_RETURN_SUCCESS) {
    stun_debug ("STUN transaction failed: couldn't create transport.");
    bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
    goto done;
  }

  val = stun_trans_send (&trans, req_buf, len);
  if (val < -1) {
    stun_debug ("STUN transaction failed: couldn't send request.");
    bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
    goto done;
  }

  stun_timer_start (&timer, STUN_TIMER_DEFAULT_TIMEOUT,
      STUN_TIMER_DEFAULT_MAX_RETRANSMISSIONS);
  stun_debug ("STUN transaction started (timeout %dms).",
      stun_timer_remainder (&timer));

  do
  {
    for (;;) {
      unsigned delay = stun_timer_remainder (&timer);
      ret = stun_trans_poll (&trans, delay);
      if (ret == STUN_USAGE_TRANS_RETURN_RETRY) {
        switch (stun_timer_refresh (&timer)) {
          case STUN_USAGE_TIMER_RETURN_TIMEOUT:
            stun_debug ("STUN transaction failed: time out.");
            bind_ret = STUN_USAGE_BIND_RETURN_TIMEOUT; // fatal error!
            goto done;
          case STUN_USAGE_TIMER_RETURN_RETRANSMIT:
            stun_debug ("STUN transaction retransmitted (timeout %dms).",
                stun_timer_remainder (&timer));
            val = stun_trans_send (&trans, req_buf, len);
            if (val <  -1) {
              stun_debug ("STUN transaction failed: couldn't resend request.");
              bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
              goto done;
            }
            continue;
          case STUN_USAGE_TIMER_RETURN_SUCCESS:
          default:
            /* Fall through. */
            break;
        }
      }
      val = stun_trans_recv (&trans, buf, sizeof (buf));
      if (val >= 0) {
        break;
      }
    }

    valid = stun_agent_validate (&agent, &msg, buf, val, NULL, NULL);
    if (valid == STUN_VALIDATION_UNKNOWN_ATTRIBUTE)
    {
      bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
      goto done;
    }

    if (valid != STUN_VALIDATION_SUCCESS) {
      ret = STUN_USAGE_TRANS_RETURN_RETRY;
    } else {
      bind_ret = stun_usage_bind_process (&msg, (struct sockaddr *)  addr,
          addrlen, (struct sockaddr *) &alternate_server, &alternate_server_len);
      if (bind_ret == STUN_USAGE_BIND_RETURN_ALTERNATE_SERVER) {
        stun_trans_deinit (&trans);

        assert (alternate_server.ss_family != AF_UNSPEC);

        ret = stun_trans_create (&trans, SOCK_DGRAM, 0,
            (struct sockaddr *) &alternate_server, alternate_server_len);

        if (ret != STUN_USAGE_TRANS_RETURN_SUCCESS) {
          bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
          goto done;
        }

        val = stun_trans_send (&trans, req_buf, len);
        if (val < -1)
        {
          bind_ret = STUN_USAGE_BIND_RETURN_ERROR;
          goto done;
        }

        stun_timer_start (&timer, STUN_TIMER_DEFAULT_TIMEOUT,
            STUN_TIMER_DEFAULT_MAX_RETRANSMISSIONS);
        ret = STUN_USAGE_TRANS_RETURN_RETRY;
      } else if (bind_ret ==  STUN_USAGE_BIND_RETURN_INVALID) {
        ret = STUN_USAGE_TRANS_RETURN_RETRY;
      } else {
        break;
      }
    }
  }
  while (ret == STUN_USAGE_TRANS_RETURN_RETRY);

done:
  if (trans.fd != -1)
    stun_trans_deinit (&trans);

  return bind_ret;
}
