/***************************************************************************
 *
 * Copyright (c) 2000, 2001 BalaBit IT Ltd, Budapest, Hungary
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: io.c,v 1.28.2.5.2.3 2001/09/19 19:22:16 bazsi Exp $
 *
 * Author  : Bazsi
 * Auditor : kisza
 * Last version: 1.15
 * Notes:
 *    - at messages: no process identifier!
 *
 ***************************************************************************/

#include <zorp/io.h>
#include <zorp/log.h>
#include <zorp/freeq.h>

#include <sys/types.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>

#define CORE_ERROR "core.error"
#define CORE_DEBUG "core.debug"

/*+ 

  ZIORealWatchFD is an implementation of the ZIOWatchFD interface. It
  uses standard glib hook mechanisms, and g_main loop, and calls its
  registered callback upon receipt of the given event.

  +*/
typedef struct _ZIORealWatchFD
{
  GIOCondition cond;
  GPollFD pollfd;
  ZWatchFDFunc callback;
} ZIORealWatchFD;

/*+ A callback registered as the prepare function of our GSource. +*/
static gboolean 
z_io_watch_fd_prepare(gpointer source,
		      GTimeVal *current_time,
		      gint *timeout,
		      gpointer user_data)
{
  ZIORealWatchFD *fd = (ZIORealWatchFD *) source;

#if 0
  z_trace(NULL, "watch_prepare, fd=%d, cond=%x", fd->pollfd.fd, fd->cond);
#endif
  fd->pollfd.events = fd->cond;
  fd->pollfd.revents = 0;
  *timeout = -1;
  return FALSE;
}

/*+ Returns TRUE if our source has some data available. +*/
static gboolean
z_io_watch_fd_check(gpointer source_data,
		    GTimeVal *current_time,
		    gpointer user_data)
{
  ZIORealWatchFD *fd = (ZIORealWatchFD *) source_data;

#if 0
  z_trace(NULL, "z_io_watch_fd_check, fd=%d, cond=%x, revents=%x", fd->pollfd.fd, fd->cond, fd->pollfd.revents);
#endif
  return !!fd->pollfd.revents;
}

/*+ Called by glib if our source has some data available to dispatch the pending event. +*/
static gboolean
z_io_watch_fd_dispatch(gpointer source_data,
                          GTimeVal *current_time,
                          gpointer user_data)
{
  ZIORealWatchFD *watch = (ZIORealWatchFD *) source_data;
  
#if 0
  z_trace(NULL, "z_io_watch_fd_dispatch, fd=%d, cond=%x, revents=%x", watch->pollfd.fd, watch->cond, watch->pollfd.revents);
#endif
  return (*watch->callback)(watch->pollfd.fd, watch->pollfd.revents, user_data);
}

/*+ Called by glib to destroy a source instance. +*/
static void
z_io_watch_fd_destroy_source(gpointer source_data)
{
  ZIORealWatchFD *watch = (ZIORealWatchFD *) source_data;

  g_main_remove_poll(&watch->pollfd);
  g_free(source_data);
}

/*+ Source functions associated with our data source. +*/
GSourceFuncs watch_fd_funcs = 
{
  z_io_watch_fd_prepare,
  z_io_watch_fd_check,
  z_io_watch_fd_dispatch,
  z_io_watch_fd_destroy_source
};


/*+
  
  This function creates a new instance of ZIOWatchFD, capable of
  monitoring the given file descriptor. Upon the occurance of the
  specified I/O condition, the passed callback function is called.

  Parameters:
     fd         fd to monitor
     cond       cond to look for
     callback   function to call
     user_data  opaque pointer to pass to callback
  +*/
ZIOWatchFD *
z_io_watch_fd_new(int fd, 
		  GIOCondition cond,
		  ZWatchFDFunc callback, 
		  gpointer user_data)
{
  ZIORealWatchFD *watch = g_new0(ZIORealWatchFD, 1);
  
  watch->pollfd.fd = fd;
  watch->pollfd.events = cond;
  watch->pollfd.revents = 0;
  watch->callback = callback;
  watch->cond = cond;
  
  g_main_add_poll(&watch->pollfd, 0);
  g_source_add(G_PRIORITY_DEFAULT, 0, &watch_fd_funcs, watch, user_data, NULL);
  return (ZIOWatchFD *) watch;
}

/*+ Destroy the given ZIOWatchFD instance +*/
void 
z_io_watch_fd_destroy(ZIOWatchFD *watch)
{
  g_source_remove_by_source_data(watch);
}

/*+
  
  The ZIOConnect interface can be used to connect a socket to a given
  destination, using the given local address. The connection establishment
  is started asynchronously, and a callback is invoked when upon completition.
  When the operation is completed this instance is automatically freed.

  +*/
typedef struct _ZIORealConnect 
{
  ZSockAddr *local;
  ZSockAddr *remote;
  ZIOWatchFD *watch;
  int sock;
  ZConnectFunc callback;
  gpointer user_data;
  gint refcnt;
  GStaticRecMutex lock;
} ZIORealConnect;

/*+ 

  Private callback function, registered to a ZIOWatchFD when the
  socket becomes writeable, e.g. when the connection is established.

  +*/
static gboolean 
z_io_connect_connected(int fd,
                       GIOCondition condition,
                       gpointer data)
{
  ZIORealConnect *self = (ZIORealConnect *) data;
  int error = 0;
  socklen_t errorlen = sizeof(error);
  ZConnectFunc callback;

  z_enter();  
  if (!self->callback)
    {
      /* we have been called */
      z_leave();
      return FALSE;
    }
  if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &errorlen) == -1)
    {
      /*LOG
        This message indicates that getsockopt(SOL_SOCKET, SO_ERROR)
        failed for the given fd. This system call should never fail,
        so if you see this, it's probably an error.
       */
      z_log(NULL, CORE_ERROR, 4, "getsockopt(SOL_SOCKET, SO_ERROR) failed for connecting socket, ignoring; fd='%d', error='%m'", fd);
    }
  if (error)
    {
      char buf[128];
      
      /*LOG
        Attempting to connect to the indicated remote endpoint failed.
       */
      z_log(NULL, CORE_ERROR, 1, "Connection to remote end failed; remote='%s', error='%s'", z_sockaddr_format(self->remote, buf, sizeof(buf)), g_strerror(error));
      close(fd);
      fd = -1;
    }
  else
    {
      z_fd_set_nonblock(fd, 0);
      z_fd_set_keepalive(fd, 1);
    }
  self->sock = -1;
  
  /* our callback frees us */
  callback = self->callback;
  self->callback = NULL;
  callback(fd, self->user_data);
  
  z_leave();
  return FALSE;
}

/*+

  Public function to start a connection establishment after the connection
  object is allocated.

  +*/
ZSockAddr *
z_io_connect_start(ZIOConnect *s)
{
  ZIORealConnect *self = (ZIORealConnect *) s;
  ZSockAddr *local = NULL;
  gchar sabuf[1500];
  gint sabuflen = sizeof(sabuf);

  z_enter();
  if (self->watch)
    {
      /* don't connect twice */
      z_leave();
      return NULL;
    }
  
  if (z_connect(self->sock, self->remote) != G_IO_ERROR_NONE && errno != EINPROGRESS)
    {
      char buf[128];
      
      /*LOG
        Attempting to connect to the indicated remote endpoint failed.
       */
      z_log(NULL, CORE_ERROR, 2, "Connection to remote end failed; remote='%s', error='%m'", z_sockaddr_format(self->remote, buf, sizeof(buf)));
      
      /* our callback always frees us */
      /* self->callback(-1, self->user_data); */
      z_leave();
      return NULL;
    }

  if (getsockname(self->sock, (struct sockaddr *) sabuf, &sabuflen) < 0)
    {
      /*LOG
        This message indicates that a call to getsockname() failed with
        the included error condition.
       */
      z_log(NULL, CORE_ERROR, 4, "getsockname(): failed; fd='%d' error='%m'", self->sock);
    }
  else
    {
      /* it contained the bind address, we now have an exact value */
      local = self->local;
      self->local = NULL;
      z_sockaddr_unref(local);
      self->local = z_sockaddr_new((struct sockaddr *) sabuf, sabuflen);
      local = z_sockaddr_ref(self->local);
    }

  z_io_connect_ref(s);
  self->watch = z_io_watch_fd_new(self->sock, G_IO_OUT, z_io_connect_connected, self);
  z_io_connect_unref(s);
  z_leave();
  return local;
}

/*+
  
  Public function to create a ZIORealConnect instance.

  Parameters:
    local           local address to bind to
    remote          remote address to connect to
    callback        function to call when the connection is established
    user_data       opaque pointer to pass to callback

  Returns:
    The allocated instance.

  +*/
ZIOConnect *
z_io_connect_new(ZSockAddr *local, 
                 ZSockAddr *remote,
		 ZConnectFunc callback,
		 gpointer user_data)
{
  ZIORealConnect *self = g_new0(ZIORealConnect, 1);

  z_enter();
  self->refcnt = 1;
  self->local = z_sockaddr_ref(local);
  self->remote = z_sockaddr_ref(remote);
  self->callback = callback;
  self->user_data = user_data;
  self->sock = socket(remote->sa.sa_family, SOCK_STREAM, 0);
  if (self->sock < 0)
    {
      gchar buf[128];
      
      /*LOG
        This message indicates that Zorp failed to create a socket for
        establishing a connection with the indicated remote endpoint.
       */
      z_log(NULL, CORE_ERROR, 1, "Creating socket for connecting failed; family='%d', type='SOCK_STREAM', remote='%s', error='%m'", remote->sa.sa_family, z_sockaddr_format(self->remote, buf, sizeof(buf)));
      z_io_connect_unref((ZIOConnect *) self);
      z_leave();
      return NULL;
    }
  if (local && z_bind(self->sock, local) != G_IO_ERROR_NONE)
    {
      z_io_connect_unref((ZIOConnect *) self);
      z_leave();
      return NULL;
    }
  
  if (!z_fd_set_nonblock(self->sock, TRUE))
    {
      /* z_fd_set_nonblock sends log message for failure */
      
      z_io_connect_unref((ZIOConnect *) self);
      z_leave();
      return NULL;
    }
  z_leave();
  return (ZIOConnect *) self;
}

/* called from the main thread, after all pending callbacks were called */
void
z_io_connect_free_func(gpointer p)
{
  ZIORealConnect *self = (ZIORealConnect *) p;
  
  z_enter();
  g_free(self);
  z_leave();
}

/*+
 
  Public function which can be called to free up a connection
  instance. 

  Parameters:
    self      instance pointer

  Remarks: It can be called both prior to, and after the connection is
           established. It frees up all associated resources.

  Returns:
    none

  +*/
static void 
z_io_connect_free(ZIOConnect *s)
{
  ZIORealConnect *self = (ZIORealConnect *) s;
  
  z_enter();
  self->callback = NULL;
  if (self->sock != -1)
    close(self->sock);
  if (self->watch)
    z_io_watch_fd_destroy(self->watch);
  z_sockaddr_unref(self->local);
  z_sockaddr_unref(self->remote);

  /* delay freeing this instance until the end of callbacks */
  z_free_queue_add(self, z_io_connect_free_func);
  z_leave();
}

void
z_io_connect_ref(ZIOConnect *s)
{
  ZIORealConnect *self = (ZIORealConnect *) s;
  
  z_enter();
  g_static_rec_mutex_lock(&self->lock);
  self->refcnt++;
  g_static_rec_mutex_unlock(&self->lock);
  z_leave();
}

void
z_io_connect_unref(ZIOConnect *s)
{
  ZIORealConnect *self = (ZIORealConnect *) s;
  
  z_enter();
  g_static_rec_mutex_lock(&self->lock);
  if (--self->refcnt == 0)
    {
      g_static_rec_mutex_unlock(&self->lock);
      z_io_connect_free(s);
      z_leave();
      return;
    }
  g_static_rec_mutex_unlock(&self->lock);
  z_leave();
}


/*+
  
  This is an implementation of the public ZIOListen interface with
  some extra hidden attributes. It binds to the given local address,
  and waits for incoming connections. For each arriving connection a
  callback is called.

  +*/
typedef struct _ZIORealListen 
{
  ZSockAddr *local;
  ZIOWatchFD *watch;
  int sock;
  ZAcceptFunc callback;
  gpointer user_data;
  gboolean accept_one;
  gint refcnt;
  GStaticRecMutex lock;
} ZIORealListen;

/*+ Private connection passed to a ZIOWatchFD to be called when the
  socket becomes readable.
  +*/
static gboolean 
z_io_listen_accept(int fd,
                   GIOCondition condition,
                   gpointer data)
{
  ZIORealListen *self = (ZIORealListen *) data;
  int newfd;
  ZSockAddr *client;
  gboolean accept_one, rc = TRUE;
  gpointer user_data;
  ZAcceptFunc callback;

  z_enter();
  z_io_listen_ref((ZIOListen *) self);
  
  callback = self->callback;
  accept_one = self->accept_one;
  user_data = self->user_data;
  
  if (!callback)
    {
      /* we have been freed */
      z_io_listen_unref((ZIOListen *) self);
      z_leave();
      return FALSE;
    }
  if (z_accept(fd, &newfd, &client) == G_IO_ERROR_NONE)
    {
      z_fd_set_nonblock(newfd, 0);
      z_fd_set_keepalive(newfd, 1);
    }
  else
    {
      /*LOG
        This message indicates that the accept() kernel call failed. This
        usually indicates a communication problem.
       */
      z_log(NULL, CORE_ERROR, 1, "accept() failed; fd='%d', error='%m'", fd);
      newfd = -1;
      client = NULL;
    }
    
  callback(newfd, client, accept_one, user_data);

  if (accept_one)
    {
      /* unref our previosly claimed reference */
      z_io_listen_unref((ZIOListen *) self);
      
      /* our callback _must_ free its reference to us if accept_one is TRUE */
      rc = FALSE;
    }
  z_leave();
  return rc;
}

/*+

  This function creates a new ZIOListen instance, internally it uses
  the ZIORealListen structure to represent it's private data.

  Parameters:
    local            address to bind to
    callback         function to call, when an incoming connection is established
    user_data        opaque pointer passed to callback
    accept_one       allow only a single connection to be accepted
    backlog          backlog value to pass to listen()

  Returns:
    The newly created instance.

  +*/
ZIOListen *z_io_listen_new(ZSockAddr *local, 
			   gboolean accept_one,
			   gint backlog,
			   ZAcceptFunc callback,
			   gpointer user_data)
{
  ZIORealListen *self = g_new0(ZIORealListen, 1);
  char sabuf[1500];
  socklen_t sabuflen = sizeof(sabuf);

  z_enter();
  self->refcnt = 1;
  self->callback = callback;
  self->user_data = user_data;
  self->accept_one = accept_one;
  self->sock = socket(local->sa.sa_family, SOCK_STREAM, 0);
  if (self->sock < 0)
    {
      z_io_listen_unref((ZIOListen *) self);
      z_leave();
      return NULL;
    }
  z_fd_set_nonblock(self->sock, 1);
  if (local && z_bind(self->sock, local) != G_IO_ERROR_NONE)
    { 
      z_io_listen_unref((ZIOListen *) self);
      z_leave();
      return NULL;
    }

  if (listen(self->sock, backlog) < 0)
    {
      /*LOG
        This message indicates that the listen() system call failed for
        the given fd.
       */
      z_log(NULL, CORE_ERROR, 1, "listen() failed; fd='%d', error='%m'", self->sock);
      z_io_listen_unref((ZIOListen *) self);
      z_leave();
      return NULL;
    }

  if (getsockname(self->sock, (struct sockaddr *) sabuf, &sabuflen) < 0)
    {
      /*LOG
        This message indicates that the getsockname() system call failed
        for the given fd.
       */
      z_log(NULL, CORE_ERROR, 1, "getsockname() failed; fd='%d', error='%m'", self->sock);
      z_io_listen_unref((ZIOListen *) self);
      z_leave();
      return NULL;
    }
  else
    {
      self->local = z_sockaddr_new((struct sockaddr *) sabuf, sabuflen);
    }
    
  z_leave();

  return (ZIOListen *) self;
}

void
z_io_listen_start(ZIOListen *s)
{
  ZIORealListen *self = (ZIORealListen *) s;
  
  z_enter();
  if (self->watch)
    {
      /* don't listen twice */
      z_leave();
      return;
    }
  
  /* our callback might be called immediately, which in turn may free us,
     thus the incremented reference here. */
     
  z_io_listen_ref(s); 
  self->watch = z_io_watch_fd_new(self->sock, G_IO_IN, z_io_listen_accept, self);
  z_io_listen_unref(s);
  z_leave();
  return;
}

void
z_io_listen_free_func(gpointer p)
{
  ZIORealListen *self = (ZIORealListen *) p;

  z_enter();  
  g_free(self);
  z_leave();
}

/*+
 
  Public function which can be called to free up a ZIORealListen
  instance. 

  Parameters:
    self      instance pointer

  Remarks: It can be called both prior to, and after connections are accepted.

  Returns:
    none

  +*/
void 
z_io_listen_free(ZIOListen *l)
{
  ZIORealListen *self = (ZIORealListen *) l;

  z_enter();
  self->callback = NULL;
  if (self->sock != -1)
    close(self->sock);
  if (self->watch)
    z_io_watch_fd_destroy(self->watch);
  z_sockaddr_unref(self->local);
  
  /* delayed free */
  z_free_queue_add(self, z_io_listen_free_func);
  z_leave();
}

void
z_io_listen_ref(ZIOListen *s)
{
  ZIORealListen *self = (ZIORealListen *) s;
  
  z_enter();
  g_static_rec_mutex_lock(&self->lock);
  self->refcnt++;
  g_static_rec_mutex_unlock(&self->lock);
  z_leave();
}

void
z_io_listen_unref(ZIOListen *s)
{
  ZIORealListen *self = (ZIORealListen *) s;
  
  z_enter();
  g_static_rec_mutex_lock(&self->lock);
  if (--self->refcnt == 0)
    {
      g_static_rec_mutex_unlock(&self->lock);
      z_io_listen_free(s);
      z_leave();
      return;
    }
  g_static_rec_mutex_unlock(&self->lock);
  z_leave();
}

/*+

  Sets the nonblocking state of the given fd.

  Parameters:
    fd          set nb state of this fd
    enable      enable/disable nonblocking state

  Returns
    TRUE if the operation succeeds.

  +*/
gboolean 
z_fd_set_nonblock(int fd, gboolean enable)
{
  int flags;
  if ((flags = fcntl(fd, F_GETFL)) == -1)
    return 0;
  if (enable)
    flags |= O_NONBLOCK;
  else
    flags &= ~O_NONBLOCK;
    
  if (fcntl(fd, F_SETFL, flags) < 0)
    {
      /*LOG
        Indicates that changing the blocking behaviour for the given
        fd failed.
       */
      z_log(NULL, CORE_ERROR, 4, "Changing blocking mode failed; fd='%d', enable='%d', error='%m'", fd, enable);
      return FALSE;
    }
  return TRUE;
}

/*+
  
  Sets the TCP SO_KEEPALIVE parameter.
  
  +*/
gboolean
z_fd_set_keepalive(int fd, gboolean enable)
{
  if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(enable)) == -1)
    {
      /*LOG
        Indicates that changing the keep-alive socket option failed for
        the given reason.
       */
      z_log(NULL, CORE_ERROR, 4, "setsockopt(SOL_SOCKET, SO_KEEPALIVE) failed; fd='%d', enable='%d', error='%m'", fd, enable);
      return FALSE;
    }
  return TRUE;
}

/*+
  
  Sets the TCP SO_OOBINLINE parameter.
  
  +*/
gboolean
z_fd_set_oobinline(int fd, gboolean enable)
{
  if (setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, &enable, sizeof(enable)) == -1)
    {
      /*LOG
        This message indicates that changing the SO_OOBINLINE socket option
        failed for the given reason.
       */
      z_log(NULL, CORE_ERROR, 4, "setsockopt(SOL_SOCKET, SO_OOBINLINE) failed; fd='%d', enable='%d', error='%m'", fd, enable);
      return FALSE;
    }
  return TRUE;
}
