/*
 * Copyright (C), 2000-2007 by the monit project group.
 * All Rights Reserved.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif

#ifdef HAVE_STRING_H
#include <string.h>
#endif

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif 

#ifdef HAVE_STRINGS_H
#include <strings.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "net.h"
#include "ssl.h"
#include "monitor.h"
#include "socket.h"



/**
 * Implementation of the socket interface.
 * 
 * @author Jan-Henrik Haukeland, <hauk@tildeslash.com>
 * @version \$Id: socket.c,v 1.63 2007/10/02 13:58:49 hauk Exp $
 * @file
 */


/* ------------------------------------------------------------- Definitions */

#define TYPE_LOCAL   0
#define TYPE_ACCEPT  1
#define RBUFFER_SIZE 1024

struct Socket_T {
  int port;
  int type;
  int socket;
  char *host;
  Port_T Port;
  int timeout;
  int connection_type;
  ssl_connection *ssl;
  ssl_server_connection *sslserver;
  int length;
  int offset;
  unsigned char buffer[RBUFFER_SIZE+1];
};


/* -------------------------------------------------------------- Prototypes */


static int fill(Socket_T S, int timeout);


/* ------------------------------------------------------------------ Public */


/**
 * Create a new Socket opened against host:port. The returned Socket
 * is a connected socket. This method can be used to create either TCP
 * or UDP sockets and the type parameter is used to select the socket
 * type. If the use_ssl parameter is TRUE the socket is created using
 * SSL. Only TCP sockets may use SSL.
 * @param host The remote host to open the Socket against. The host
 * may be a hostname found in the DNS or an IP address string.
 * @param port The port number to connect to
 * @param type The socket type to use (SOCKET_TCP or SOCKET_UPD)
 * @param use_ssl If TRUE the socket is created supporting SSL
 * @param timeout The timeout value in seconds
 * @return The connected Socket or NULL if an error occurred
 */
Socket_T socket_new(const char *host, int port, int type, int use_ssl,
                    int timeout) {
  
  Ssl_T ssl;    
  
  ssl.use_ssl= use_ssl;
  ssl.version= SSL_VERSION_AUTO;
  ssl.certmd5= NULL;
  
  return socket_create_t(host, port, type, ssl, timeout);
  
}


/**
 * Factory method for creating a new Socket from a monit Port object
 * @param port The port object to create a socket from
 * @return The connected Socket or NULL if an error occurred
 */
Socket_T socket_create(void *port) {
  
  int s;
  Port_T p= port;
  
  ASSERT(port);
  
  if((s= create_generic_socket(p)) != -1) {
    
    Socket_T S= NULL;
    
    NEW(S);
    S->socket= s;
    S->length= 0;
    S->offset= 0;
    S->type= p->type;
    S->port= p->port;
    S->timeout= p->timeout;
    S->connection_type= TYPE_LOCAL;
    
    if(p->family==AF_UNIX) {
      S->host= xstrdup(LOCALHOST);
    } else {
      S->host= xstrdup(p->hostname);
    }
    
    if(p->SSL.use_ssl && !socket_switch2ssl(S, p->SSL)) {
      socket_free(&S);
      return NULL;
    }
    
    S->Port= port;
    return S;
  }
  
  return NULL;
}


/**
 * Create a new Socket opened against host:port with an explicit
 * ssl value for connect and read. Otherwise, same as socket_new()
 * @param host The remote host to open the Socket against. The host
 * may be a hostname found in the DNS or an IP address string.
 * @param port The port number to connect to
 * @param type The socket type to use (SOCKET_TCP or SOCKET_UPD)
 * @param ssl Options for SSL
 * @param timeout The timeout value in seconds
 * @return The connected Socket or NULL if an error occurred
 */
Socket_T socket_create_t(const char *host, int port, int type, Ssl_T ssl,
                         int timeout) {
  
  int s;
  int proto= type==SOCKET_UDP?SOCK_DGRAM:SOCK_STREAM;
  
  ASSERT(host);
  ASSERT((type==SOCKET_UDP)||(type==SOCKET_TCP));
  if(ssl.use_ssl) {
    ASSERT(type==SOCKET_TCP);
  }
  ASSERT(timeout>0);
  
  if((s= create_socket(host, port, proto, timeout)) != -1) {
    
    Socket_T S= NULL;
    
    NEW(S);
    S->socket= s;
    S->length= 0;
    S->offset= 0;
    S->port= port;
    S->type= proto;
    S->timeout= timeout;
    S->host= xstrdup(host);
    S->connection_type= TYPE_LOCAL;
    
    if(ssl.use_ssl && !socket_switch2ssl(S, ssl)) {
      socket_free(&S);
      return NULL;
    }
    
    return S;
  }
  
  return NULL;
}


/**
 * Factory method for creating a Socket object from an accepted
 * socket. The given socket must be a socket created from accept(2).
 * If the sslserver context is non-null the socket will support
 * ssl. This method does only support TCP sockets.
 * @param socket The accepted socket
 * @param remote_host The remote host from where the socket connection
 * originated
 * @param port The localhost port number from where the connection
 * arrived.
 * @param sslserver A ssl server connection context, may be NULL
 * @return A Socket or NULL if an error occurred
 */
Socket_T socket_create_a(int socket, const char *remote_host,
                         int port, void *sslserver) {
  
  Socket_T S;
  
  ASSERT(socket>=0);
  ASSERT(remote_host);
  
  NEW(S);
  S->length= 0;
  S->offset= 0;
  S->port= port;
  S->socket= socket;
  S->type= SOCK_STREAM;
  S->timeout= NET_TIMEOUT;
  S->host= xstrdup(remote_host);
  S->connection_type= TYPE_ACCEPT;
  
  if(sslserver) {
    S->sslserver= sslserver;
    if(! (S->ssl= insert_accepted_ssl_socket(S->sslserver))) {
      goto ssl_error;
    }
    if(! embed_accepted_ssl_socket(S->ssl, S->socket)) {
      goto ssl_error;
    }
  }
  
  return S;
  
ssl_error:
    socket_free(&S);
  return NULL;
  
}


/**
 * Destroy a Socket object. Close the socket and release allocated
 * resources. 
 * @param S A Socket object reference
 */
void socket_free(Socket_T *S) {
  
  ASSERT(S && *S);
  
#ifdef HAVE_OPENSSL
  if((*S)->ssl && (*S)->ssl->handler) {
    if((*S)->connection_type==TYPE_LOCAL) {
      close_ssl_socket((*S)->ssl);
      delete_ssl_socket((*S)->ssl);
    } else if((*S)->connection_type==TYPE_ACCEPT && (*S)->sslserver) {
      close_accepted_ssl_socket((*S)->sslserver, (*S)->ssl);
    }
  }
#endif
  
  close_socket((*S)->socket);
  FREE((*S)->host);
  FREE(*S);
  
}


/* ------------------------------------------------------------ Properties */


/**
 * Returns TRUE if the socket is ready for i|o
 * @param S A Socket object
 * @return TRUE if the socket is ready otherwise FALSE 
 */
int socket_is_ready(Socket_T S) {
  
  ASSERT(S);
  
  switch(S->type) {
    
    case SOCK_STREAM:
      return check_socket(S->socket);
      
    case SOCK_DGRAM:
      return check_udp_socket(S->socket);
      
    default:
      break;
  }
  
  return FALSE;
  
}


/**
 * Get the underlying socket descriptor
 * @param S A Socket object
 * @return The socket descriptor
 */
int socket_get_socket(Socket_T S) {
  
  ASSERT(S);
  
  return S->socket;
  
}


/**
 * Get the type of this socket.
 * @param S A Socket object
 * @return The socket type
 */
int socket_get_type(Socket_T S) {
  
  ASSERT(S);
  
  return S->type;
  
}


/**
 * Get the Port object used to create this socket. If no Port object
 * was used this method returns NULL.
 * @param S A Socket object
 * @return The Port object or NULL
 */
void *socket_get_Port(Socket_T S) {
  
  ASSERT(S);
  
  return S->Port;
  
}


/**
 * Get the remote port number the socket is connected to
 * @param S A Socket object
 * @return The remote host's port number
 */
int socket_get_remote_port(Socket_T S) {
  
  ASSERT(S);
  
  return S->port;
  
}


/**
 * Get the remote host this socket is connected to. The host is either
 * a host name in DNS or an IP address string.
 * @param S A Socket object
 * @return The remote host
 */
const char *socket_get_remote_host(Socket_T S) {
  
  ASSERT(S);
  
  return S->host;
  
}


/* ---------------------------------------------------------------- Public */


/**
 * Switches a connected socket to ssl.
 * @param S The already connected socket
 * @param ssl Options for ssl
 * @return TRUE if ssl is ready otherwise FALSE
 */
int socket_switch2ssl(Socket_T S, Ssl_T ssl)  {

  if(! (S->ssl= new_ssl_connection(NULL, ssl.version)))
    return FALSE;

  if(! embed_ssl_socket(S->ssl, S->socket))
    return FALSE;

  if(ssl.certmd5 && !check_ssl_md5sum(S->ssl, ssl.certmd5)) {
    LogError("md5sum of certificate does not match!");
    return FALSE;
  }

  return TRUE;
}


/**
 * Writes a character string. Use this function to send text based
 * messages to a client.
 * @param S A Socket_T object
 * @param m A String to send to the client
 * @return The bytes sent or -1 if an error occured
 */
int socket_print(Socket_T S, const char *m, ...) {
  
  int n;
  long l;
  va_list ap;
  char *buf= NULL;
  
  ASSERT(S);
  ASSERT(m);
  
  va_start(ap, m);
  buf= Util_formatString(m, ap, &l);
  va_end(ap);
  
  n= socket_write(S, buf, l);
  FREE(buf);
  
  return n;
  
}


/**
 * Write size bytes from the buffer b.
 * @param S A Socket_T object
 * @param b The data to be written
 * @param size The size of the data in b
 * @return The bytes sent or -1 if an error occured
 */
int socket_write(Socket_T S, void *b, int size) {
  
  int n= 0;
  void *p= b;
  
  ASSERT(S);
  
  /* Clear any extra data read from the server */
  socket_reset(S);

  while(size > 0) {
    
    if(S->ssl) {
      n= send_ssl_socket(S->ssl, p, size, S->timeout);
    } else {
      if(S->type==SOCK_DGRAM)
        n= udp_write(S->socket,  p, size, S->timeout);
      else
        n= sock_write(S->socket,  p, size, S->timeout);
    }
    
    if(n <= 0) break;
    p+= n;
    size-= n;
    
  }
  
  if(n < 0) {
    /* No write or a partial write is an error */
    return -1;
  }
  
  return  (int)(p - b);
  
}


/**
 * Read a single byte. The byte is returned as an int in the range 0
 * to 255.
 * @param S A Socket_T object
 * @return The byte read, or -1 if the end of the stream has been reached
 */
int socket_read_byte(Socket_T S) {
  
  ASSERT(S);
  
  if(S->offset >= S->length) {
    if(fill(S, S->timeout) <= 0)
      return -1;
  }
  
  return S->buffer[S->offset++];
  
}


/**
 * Reads size bytes and stores them into the byte buffer pointed to by b.
 * @param S A Socket_T object
 * @param b A Byte buffer
 * @param size The size of the buffer b
 * @return The bytes read or -1 if an error occured
 */
int socket_read(Socket_T S, void *b, int size) {
  
  int c;
  unsigned char *p= b;
  
  ASSERT(S);
  
  while((size-- > 0) && ((c= socket_read_byte(S)) >= 0)) { 
    *p++= c;
  }
  
  return  (long)p - (long)b;
  
}


/**
 * Reads in at most one less than size <code>characters</code> and
 * stores them into the buffer pointed to by s. Reading stops after
 * an EOF or a newline.  If a newline is read, it is stored into the
 * buffer.   A '\0' is stored after the last character in the buffer.
 * @param S A Socket_T object
 * @param s A character buffer to store the string in
 * @param size The size of the string buffer, s
 * @return s on success, and NULL on error or when end of file occurs
 * while no characters have been read.
 */
char *socket_readln(Socket_T S, char *s, int size) {
  
  int c;
  unsigned char *p= (unsigned char *)s;
  
  ASSERT(S);
  
  while(--size && ((c= socket_read_byte(S)) > 0)) {
    *p++= c;
    if(c == '\n' || c == '\0')
      break;
  }
  
  *p= 0;
  
  if(*s)
    return s;
  
  return NULL;
  
}


/**
 * Clears any data that exists in the input buffer
 * @param S A Socket_T object
 */
void socket_reset(Socket_T S) {

  ASSERT(S);
  
  /* Throw away any pending incomming data */
  while(fill(S, 0) > 0);
  S->offset= 0;
  S->length= 0;
  
}


/* --------------------------------------------------------------- Private */


/*
 * Fill the internal buffer. If an error occurs or if the read
 * operation timed out -1 is returned.
 * @param S A Socket object
 * @param timeout The number of seconds to wait for data to be read
 * @return TRUE (the length of data read) or -1 if an error occured
 */
static int fill(Socket_T S, int timeout) {
  
  int n;
  
  S->offset= 0;
  S->length= 0;
  /* Optimizing, assuming a request/response pattern and that a udp_write
     was issued before we are called, we don't have to wait for data */
  if(S->type==SOCK_DGRAM) timeout= 0; 
  
  /* Read as much as we can, but only block on the first read */
  while(RBUFFER_SIZE > S->length) {
    
    if(S->ssl) {
      n= recv_ssl_socket(S->ssl, S->buffer + S->length, 
                         RBUFFER_SIZE-S->length, timeout);
    } else {
      n= sock_read(S->socket, S->buffer + S->length, 
                   RBUFFER_SIZE-S->length, timeout);
    }
    
    timeout= 0;
    
    if(n > 0) {
      S->length+= n;
      continue;
    }  else if(n < 0) {
      if(errno == EAGAIN || errno == EWOULDBLOCK || S->type==SOCK_DGRAM) break;
      return -1;
    } else
      break;

  }
  
  return S->length;
  
}
