/*
   <h3>A library package for handling sockets</h3>
   <p>- <i>Written by M.C.Gregorie, 2003.</i></p>

   <p>The functions in this package are wrappers round the UNIX sockets
   library. Their purpose is to minimise the repetitive coding involved
   in using the raw sockets interface. There are three groups of functions
   in this package:</p>

   <p>Functions for use in servers that accept incoming connections:</p>
   <ul>
      <li>skt_listen()</li>
      <li>skt_accept()</li>
      <li>skt_closelistener()</li>
   </ul>
   
   <p>Functions for use in clients that make outgoing connections:</p>
   <ul>
      <li>skt_connect()</li>
   </ul>
   
   <p>General purpose functions that can be used in clients or servers:</p>
   <ul>
      <li>skt_debug()</li>
      <li>skt_error()</li>
      <li>skt_send()</li>
      <li>skt_receive()</li>
      <li>skt_close()</li>
      <li>skt_getname()</li>
      <li>skt_getport()</li>
   </ul>
   
   <p>Programs can use all three groups if they must accept connections
   as well as making connections to other programs.</p>

   <p>This package operates transparently over named pipes or TCP/IP sockets.
   The only place where this needs to be specified is in the parameters of
   <i>skt_xxx()</i> functions that open connections to other programs.</p>

   <p>This collection of functions is compiled to form part
   of the <b>libenviron.a</b> library, which must be linked
   if any of of its constituent functions are called.</p>

   <h3>Compilation</h3>
        
   <p>The calling programs should include <i>skt.h</i> and must be
   linked using <i>-lenviron</i>.</p>

   <kbd>
   <b>environ - C programming support functions</b><br>
   Copyright (C) 2005  Martin C Gregorie
   
   This program is free software; you can redistribute it and/or
   modify it under the terms of the Lesser GNU General Public License.
   
   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 Lesser GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
   </kbd>
  
   <p>Contact Martin Gregorie at <i>martin@gregorie.org</i></p>
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>

#include "skt.h"

/*
   Private  definitions
   ====================
*/
#define SKT_UNSET       0
#define SKT_PIPE        1
#define SKT_PORT        2
#define MAXPENDING      5
#define SEPARATOR       ':'
#define MAXLENGTHCHARS  6
#define MAXLENGTHVAL    999999

/*
   Globals used only within this source file
   =========================================
*/
static char *errmsg         = "";
static int  debug           = 0;
static int  listentype      = SKT_UNSET;

static struct sockaddr_un   pipe;
static int                  pipelen;
static struct sockaddr_in   port;
static int                  portlen;

/*
   Functions
   =========
*/

/* 
   Set debugging conditions.
   When the debug level is zero no tracing occurs.
   A debug level of 1 traces calls to the functions in this
   package. It is useful for debugging the calling program.
   A debug level of 2 or greater is used to trace the
   internals of the more complex functions on this package.
*/
void skt_debug(int d)
{
   if (d < 0)
      d = 0;
      
   debug = d;

   if (debug)
      logmsg("skt_debug(%d)", debug);
}

/*
   Return a string describing the last error detected.
*/
char *skt_error(void)
{
   char  *b;
   char  *e;
   int   n;

   e = strerror(errno);
   n = strlen(errmsg) + strlen(e) + 5;
   b = (char*)malloc(n);
   if (b == 0)
      b = "String allocation error in skt_error()";
   else
      if (errno == 0)
         sprintf(b, "%s", errmsg);
      else
         sprintf(b, "%s: %s", errmsg, e);

   if (debug)
      logmsg("%s = skt_error()", b);
      
   return b;
}

/*
   Create a listener for a server. The listener can operate on a
   TCP/IP socket or a named pipe. If <u>portnum</u> is zero a
   named pipe is created from <u>name</u>. If non-zero,
   <u>portnum</u> is the port to be used. It returns the fd
   of the listener on success and a negative number if an error
   was detected.
   
   <b>NOTE:</b> there can be only one active listener per
   process but it can be closed and reopened.
*/
int skt_listen(char *name, int portnum)
{
   int                  listener;
   short int            portany;
   char                 *sockoptname = "                    ";
   int                  sockoptlen;

   if (debug)
      logmsg("skt_listen(%s, %d)", name, portnum);

   /*
      Check that there is no current listener for this process
      and report an error if there is.
   */
   if (listentype != SKT_UNSET)
   {
      if (debug > 1)
         logmsg("Listener already active for this process");

      listener = -1;
      errno = EPERM;
      if (debug)
         logmsg("%d = skt_listen(%s, %d)", listener, name, portnum);
   
      return listener;
   }

   /*
      Now set up a listener socket using either a port or a
      named pipe depending on whether the port number is zero
   */
   if (portnum)
   {
      /*
         Create a socket
      */
      if ((listener = socket(AF_INET, SOCK_STREAM, 0)) < 0)
      {
         errmsg = "error creating listener socket";
      }
      else
      {
         if (debug > 1)
            logmsg("Listener socket created");

         /*
            Set the socket options to allow re-use
         */
         sockoptlen = strlen(sockoptname);
         if (setsockopt(listener,
                        SOL_SOCKET,
                        SO_REUSEADDR,
                        sockoptname,
                        sockoptlen) < 0)
         {
            errmsg = "setting socket options";
            listener = -1;
         }
         else
         {
            /*
               Create server address using the wildcard address
            */
            memset(&port, 0, sizeof(struct sockaddr_in));
            port.sin_family = AF_INET;
            port.sin_port = htons(portnum);
            portlen = sizeof(struct sockaddr_in);
            portany = INADDR_ANY;
            memcpy(&port.sin_addr, &portany, sizeof(portany));
      
            /*
               Bind the socket to the address
            */
            if (bind(listener, (struct sockaddr *)&port, portlen) < 0)
            {
               errmsg = "error binding socket";
               listener = -1;
            }
            else
            {
               if (debug > 1)
                  logmsg("Listener bound to port: %d", portnum);
      
               /*
                  Listen for connections
               */
               if (listen(listener, MAXPENDING) < 0)
               {
                  errmsg = "error listening for connections";
               }
               else
               {
                  if (debug > 1)
                     logmsg("Listening for connections");
                     
                  /*
                     Indicate that a listener tis now active.
                  */
                  listentype = SKT_PORT;
               }
            }
         }
      }
   }
   else
   {
      /*
         Remove the named pipe if it exists and create a new one
      */
      unlink(name);
      
      /*
         Create a socket
      */
      if ((listener = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
      {
         errmsg = "error creating listener socket";
      }
      else
      {
         if (debug > 1)
            logmsg("Listener socket created");
   
         /*
            Create server address
         */
         memset(&pipe, 0, sizeof(struct sockaddr_un));
         pipe.sun_family = AF_UNIX;
         strcpy(pipe.sun_path, name);
         pipelen = sizeof(pipe.sun_family) + strlen(pipe.sun_path);

         /*
            Bind the socket to the address
         */
         if (bind(listener, (struct sockaddr *)&pipe, pipelen) < 0)
         {
            errmsg = "error binding socket";
            listener = -1;
         }
         else
         {
            if (debug > 1)
               logmsg("Listener bound to pipe: %s", name);
      
            /*
               Listen for connections
            */
            if (listen(listener, MAXPENDING) < 0)
            {
               errmsg = "error listening";
            }

            if (debug > 1)
               logmsg("Listening for connections");

            /*
               Indicate that a listener tis now active and 
               prime skt_closelistener() to delete the pipe
            */
            listentype = SKT_PIPE;
         }
      }
   }

   if (debug)
      logmsg("%d = skt_listen(%s, %d)", listener, name, portnum);
   
   return listener;
}

/*
   Create a server connection for an incoming request
   detected by the listener. <u>listener</u> is the fd
   of the listener socket. Returns the fd of the connection
   that has been accepted on success or a negative number
   if an error was detected. 
*/ 
int  skt_accept(int listener)
{
   int   connection;

   if (listentype == SKT_PIPE)
   {
      if ((connection = accept(listener,
                               (struct sockaddr *)&pipe,
                               &pipelen)) < 0)
      {
         errmsg = "error accepting named pipe connection";
      }
   }
      
   if (listentype == SKT_PORT)
   {
      if ((connection = accept(listener,
                               (struct sockaddr *)&port,
                               &pipelen)) < 0)
      {
         errmsg = "error accepting port connection";
      }
   }

   if (debug)
      logmsg("%d = skt_accept(%d)", connection, listener);

   return connection;
}

/*
   Used by a client to open a connection to a server.
   Multiple simultaneous connections are permitted.
   If <u>portnum</u> is non zero it will attempt to connect to
   the host given by <u>name</u> using the port in <u>portnum</u>.
   If <u>portnum</u> is zero it will attempt to connect to
   a named pipe using the name in <u>name</u>.

   It returns the fd of the connection on success or a negative number
   if the connection attempt failed.
*/
int skt_connect(char *name, int portnum)
{
   int                  skt;
   struct sockaddr_un   pipe;
   int                  pipelen;
   struct sockaddr_in   port;
   int                  portlen;
   char                 *hostname;
   struct hostent       *hp;
   int                  n;

   if (debug)
      logmsg("skt_connect(%s, %d)", name, portnum);

   if (portnum)
   {
      /*
         Look up host's address
      */
      if ((hp = gethostbyname(name)) == NULL)
      {
         errmsg = "unknown host";
         skt = -1;
      }
      else
      {
         if ((skt = socket(AF_INET, SOCK_STREAM, 0)) < 0)
         {
            errmsg = "error creating socket";
         }
         else
         {
            if (debug > 1)
               logmsg("Socket created");

            /*
               Create the server address
            */
            memset(&port, 0, sizeof(struct sockaddr_in));
            port.sin_family = AF_INET;
            port.sin_port = htons(portnum);
            memcpy(&port.sin_addr, hp->h_addr_list[0], hp->h_length);
            portlen = sizeof(struct sockaddr_in);

            /*
               Connect to the server
            */
            if (debug > 1)
               logmsg("Connecting to server");

            if (connect(skt, (struct sockaddr *)&port, portlen) < 0)
            {
               errmsg = "error connecting to server";
               skt = -1;
            }

            if (debug > 1)
               logmsg("Connected to server");
         }
      }
   }
   else
   {
      if ((skt = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
      {
         errmsg = "error creating socket";
      }
      else
      {
         if (debug > 1)
            logmsg("Socket created");

         /*
            Create the server address
         */
         memset(&pipe, 0, sizeof(struct sockaddr_un));
         pipe.sun_family = AF_UNIX;
         strcpy(pipe.sun_path, name);
         pipelen = sizeof(pipe.sun_family) + strlen(pipe.sun_path);

         /*
            Connect to the server
         */
         if (debug > 1)
            logmsg("Connecting to server");

         if (connect(skt, (struct sockaddr *)&pipe, pipelen) < 0)
         {
            errmsg = "error connecting to server";
            skt = -1;
         }
         else
         {
            if (debug > 1)
               logmsg("Connected to server");
         }
      }
   }

   if (debug)
      logmsg("%d = skt_connect(%s, %d)", skt, name, portnum);
      
   return skt;
}

/*
   Used by client or server to send a message on an open connection.
   It is a wrapper for <i>send()</i>.
   <u>skt</u> is the fd of the socket. <u>request</u> is a byte array
   containing the message to be sent. <u>length</u> is the number of
   bytes to be sent. It returns the same result codes as <i>send()</i>.
*/
int skt_send(int skt, char* request, int length)
{
   int   n;
   int   offset;
   char  *buff;

   n = -1;                       /* Assume something will fail */
   if (length > MAXLENGTHVAL)
      errno = EINVAL;
   else
   {
      buff = (char*)malloc(length + MAXLENGTHCHARS + 1);
      if (!buff)
         errno = ENOMEM;
      else
      {
         sprintf(buff, "%d%c", length, SEPARATOR);
         offset = strlen(buff);
         memcpy(buff+offset, request, length);
         n = send(skt, buff, length + offset, 0);
      }
   }
   
   if (debug)
   {
      logmsg("%d = skt_send(%d) sent %d bytes", n, skt, length);
      dumphex(stderr, buff, length + offset);
   }

   if (buff)
      free(buff);
      
   return n;
}

/*
   Used by client or server to receive a message on an open connection.
   It is a wrapper for <i>recv()</i>.
   <u>skt</u> is the fd of the socket, <u>*response</u> is the receiving
   buffer and <u>length</u> is the buffer length. It returns the same
   result codes as <i>recv()</i>.
   Negative result codes are errors and zero means the remote process
   has closed its connection.
*/
int skt_receive(int skt, char *response, int length)
{
   int n;
   int lth;
   int charsin;
   char c;
   int i;

   n = 0;
   c = 0;
   lth = 0;
   charsin = 0;
   for (i = 0; i < MAXLENGTHCHARS; i++)
   {
      n = recv(skt, &c, 1, 0);
      if (c != SEPARATOR)
         lth = lth * 10 + (c - '0');

      if (n <= 0 || c== SEPARATOR)
         break;
   }

   if (n >= 0)
   {
      if (lth >= length)
      {
         n = -1;
         errno = ENOMEM;
      }
      else
      {
         while (lth > 0)
         {
            n = recv(skt, response + charsin, lth, 0);
            
            if (n < 0)
               lth = 0;
            else
            {
               lth -= n;
               charsin += n;
            }
         }
      }
   }
   
   response[charsin] = 0;
   if (debug)
   {
      logmsg("%d = skt_receive(%d,buff,%d)", n, skt, length);
      dumphex(stderr, response, charsin);
   }

   return n;
}

/*
   Used by client or server to close an open (non-listener) connection.
*/
void skt_close(int skt)
{
   close(skt);
   if (debug)
      logmsg("skt_close(%d)", skt);
}

/*
   Used by a server to close an open listener connection for possible re-use.
   <u>skt</u> is the fd of the listener. <u>*pipe</u> is only needed if the
   listener was listening on a named pipe: it which case it is the name
   of the pipe.
*/
void skt_closelistener(int skt, char *pipe)
{
   close(skt);
   if (listentype == SKT_PIPE)
   {
      unlink(pipe);
      if (debug)
         logmsg("skt_closelistener(%d, %s): deleted named pipe %s",
                skt,
                pipe,
                pipe);
   }
   else
   {
      if (debug)
         logmsg("skt_closelistener(%d, %s)", skt, pipe);
   }

   listentype = SKT_UNSET;
}

/*
   Retrieves the host name at one end of the connection.
   <u>skt</u> is the fd of the socket, <u>locality</u>
   specifies which end of the connection is to be interrogated.
   Either LOCAL_HOST or REMOTE_HOST must be specified.
   <u>host</u> is the buffer that will receive the hostname
   and <u>lth</u> is the buffer length.
   It returns 0 on success and -1 on failure.
*/
int skt_getname(int skt, int locality, char *host, int lth)
{
   int   r = -1;
   char  *ds = "INVALID";

   union
   {
      struct sockaddr_un un;
      struct sockaddr_in in;
   } buff;

   int bufflth;
   struct hostent *he;

   bufflth = sizeof(buff);
   if (locality == LOCAL_HOST)
   {
      ds = "local";
      r = getsockname(skt, (struct sockaddr *) &buff, &bufflth);
   }
   else
   if (locality == REMOTE_HOST)
   {
      ds = "remote";
      r = getpeername(skt, (struct sockaddr *) &buff, &bufflth);
   }
   else
   {
      errmsg = "invalid locality value";
      r = -1;
   }
      
   if (r == 0)
   {
      if (buff.un.sun_family == AF_UNIX)
         strncpy(host, buff.un.sun_path, lth);
      else
      {
         he = gethostbyaddr((const char *) &buff.in.sin_addr,
                            sizeof(buff.in.sin_addr),
                            AF_INET);
         strncpy(host, he->h_name, lth);         
      }
   }

   if (debug)
      logmsg("%d = skt_getname(%d,%s,host,%d)",
             r,
             skt,
             ds,
             lth);
   return r;
}

/*
   Retrieves the port number used by the socket whose fd is in
   <u>skt</u>. <u>locality</u> specifies which end of the
   connection is to be interrogated. Either LOCAL_HOST or
   REMOTE_HOST must be specified.
   
   It returns the port number on success and -1 on failure.
*/
int skt_getport(int skt, int locality)
{
   int   r = -1;
   char  *ds = "INVALID";

   union
   {
      struct sockaddr_un un;
      struct sockaddr_in in;
   } buff;

   int bufflth;

   bufflth = sizeof(buff);
   if (locality == LOCAL_HOST)
   {
      ds = "local";
      r = getsockname(skt, (struct sockaddr *) &buff, &bufflth);
   }
   else
   if (locality == REMOTE_HOST)
   {
      ds = "remote";
      r = getpeername(skt, (struct sockaddr *) &buff, &bufflth);
   }
   else
   {
      errmsg = "invalid locality value";
      r = -1;
   }
      
   if (r == 0)
   {
      if (buff.un.sun_family == AF_UNIX)
      {
         r = -1;
         errmsg = "named pipes don't have ports";
      }
      else
         r = ntohs(buff.in.sin_port);
   }

   if (debug)
      logmsg("%d = skt_getport(%d,%s)", r, skt, ds);
   return r;
}

