/* 
   elmo - ELectronic Mail Operator

   Copyright (C) 2002, 2003, 2004 rzyjontko

   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; version 2.

   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  

   ----------------------------------------------------------------------

   These are all primitives I need to use when connecting as client.
   
*/
/****************************************************************************
 *    IMPLEMENTATION HEADERS
 ****************************************************************************/

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

#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <setjmp.h>
#include <signal.h>
#include <regex.h>

#ifdef OPENSSL_SUPPORT
# include <openssl/ssl.h>
#endif

#include "xmalloc.h"
#include "error.h"
#include "networking.h"
#include "gettext.h"
#include "rstring.h"
#include "cmd.h"
#include "debug.h"
#include "clock.h"
#include "str.h"

/****************************************************************************
 *    IMPLEMENTATION PRIVATE DEFINITIONS / ENUMERATIONS / SIMPLE TYPEDEFS
 ****************************************************************************/

#define BUFFER_SIZE (17 * 1024)
#define SEND_CHUNK_SIZE 1000

#define MIN(a,b) (((a)<(b))?(a):(b))

/****************************************************************************
 *    IMPLEMENTATION PRIVATE CLASS PROTOTYPES / EXTERNAL CLASS REFERENCES
 ****************************************************************************/
/****************************************************************************
 *    IMPLEMENTATION PRIVATE STRUCTURES / UTILITY CLASSES
 ****************************************************************************/
/****************************************************************************
 *    IMPLEMENTATION REQUIRED EXTERNAL REFERENCES (AVOID)
 ****************************************************************************/
/****************************************************************************
 *    IMPLEMENTATION PRIVATE DATA
 ****************************************************************************/

/* There is one problem with non-multi-threaded programs.  You cannot
   discard the connection while it's in progress because gethostbyname,
   and connect block until the end of execution.

   To work this around I setup a special SIGINT signal handler, and make
   a nonlocal transfer of control with longjmp function from it.

   The code should look like:

        if (setjmp (discard_connection))
                return 1;
        
        prev_handler = signal (SIGINT, connection_break);
        ret          = do_the_blocking_action ();
        signal (SIGINT, prev_handler);

   The signal handler must restore previous handler in the same manner. */

/* The prev_handler is used to restore a previous handler (which terminates
   the program execution). */
static void (*prev_handler)(int) = NULL;



/* The discard_connection is a longjmp target established before
   gethostbyname, and connect. */
static sigjmp_buf discard_connection;



#ifdef OPENSSL_SUPPORT
/* OpenSSL context. */
static SSL_CTX *ssl_ctx = NULL;
#endif


struct nlist *Net::nlist = NULL;

/****************************************************************************
 *    INTERFACE DATA
 ****************************************************************************/
/****************************************************************************
 *    IMPLEMENTATION PRIVATE FUNCTION PROTOTYPES
 ****************************************************************************/

static void read_handler (int fd);
static void write_handler (int fd);

/****************************************************************************
 *    IMPLEMENTATION PRIVATE FUNCTIONS
 ****************************************************************************/

static void
connection_break (int signum)
{
        signal (SIGINT, prev_handler);
        siglongjmp (discard_connection, 1);
}



bool
Net::resolve_host (void)
{
        struct hostent *ht;
        bool            ret;

        status = NET_RESOLVING;
        ht     = gethostbyname (server_name);

        if (! ht){
                error_ (0, _("Couldn't resolve name %s: %s"),
                        server_name, hstrerror (h_errno));
                ret = false;
        }
        else {
                server_addr = * (struct in_addr *) ht->h_addr_list[0];
                ret         = true;
        }

        if (! ret)
                status = NET_DOWN;
  
        return ret;
}



bool
Net::make_connection (bool secure)
{
        struct sockaddr_in  st;
        socklen_t           st_len = sizeof (struct sockaddr_in);
        int                 ret;

        if (status != NET_RESOLVING)
                return false;
  
        status = NET_CONNECTING;
        sock   = socket (PF_INET, SOCK_STREAM, 0);

        if (sock == -1){
                error_ (errno, _("Couldn't connect to %s (%s)"),
                        server_name, inet_ntoa (server_addr));
                return false;
        }

        st.sin_family = AF_INET;
        st.sin_addr   = server_addr;
        st.sin_port   = server_port;

        ret = connect (sock, (struct sockaddr *) & st, sizeof (st));
        
        if (ret == -1){
                error_ (errno, "%s", inet_ntoa (server_addr));
                return false;
        }

        if (getsockname (sock, (struct sockaddr *) & st, & st_len) == 0){
                local_addr = st.sin_addr;
                local_port = st.sin_port;
        }

        if (secure){
#ifdef OPENSSL_SUPPORT
                ssl = SSL_new (ssl_ctx);
                SSL_set_fd (ssl, sock);
                ret = SSL_connect (ssl);
#else
                error_ (0, "%s", _("Secure connection  requested, but "
                                   "no ssl support."));
                return false;
#endif
        }

        status = NET_READY;
        return true;
}



void
Net::shutdown_connection (void)
{
#ifdef OPENSSL_SUPPORT
        if (ssl)
                SSL_shutdown (ssl);
        else
#endif
                shutdown (sock, SHUT_RDWR);
}


bool
Net::check_read_write_ret (int ret, const char *s)
{
        if (ret < 0){
                error_ (errno, s);
                close ();
                return false;
        }

        if (ret == 0){
                error_ (errno, _("Server has closed the connection."));
                close ();
                return false;
        }
        return true;
}



void
Net::data_reader (void)
{
        int        ret;
        regmatch_t matches[1];
        
        if (read_buffer == NULL){
                read_size   = BUFFER_SIZE;
                read_fill   = 0;
                read_buffer = (char *) xmalloc (BUFFER_SIZE);
        }

        if (read_fill + 1 >= read_size){
                read_size   *= 2;
                read_buffer  = (char *) xrealloc (read_buffer, read_size);
        }

#ifdef OPENSSL_SUPPORT
        if (ssl){
                ret = SSL_read (ssl, read_buffer + read_fill,
                                read_size - read_fill - 1);
        }
        else
#endif
                ret = recv (sock, read_buffer + read_fill,
                            read_size - read_fill - 1, 0);


        if (! check_read_write_ret (ret, _("Error occured during receiving data.")))
                return;
        
        if (progress != -1)
                progress_advance (progress, ret);
        
        total_recv += ret;
        read_fill  += ret;
        read_buffer[read_fill] = '\0';

        ret = regexec (& terminator, read_buffer, 1, matches, 0);
        if (ret && ret != REG_NOMATCH){
                error_regex (ret, & terminator, NULL);
                close ();
                return;
        }

        if (ret == 0){
                int len     = read_fill;
                read_fill   = 0;
                status      = NET_READY;
                is_compiled = 0;
                regfree (& terminator);

                if (progress != -1)
                        progress_close (progress);
                progress = -1;
                
                cmd_del_readfd_handler (sock);

                /* It is absolutely crucial, that this call is the last
                   thing to do in this function.  It is because recv_fun
                   may (implicitly) modify some important data structures.
                   It may remove some readfd handlers (including this one),
                   or add a new one. It may even destroy the enet object. */
                recv_fun (read_buffer, len);
        }
}



void
Net::data_sender (void)
{
        int ret;
        int size;

        size = MIN (SEND_CHUNK_SIZE, send_size - send_sent);

#ifdef OPENSSL_SUPPORT
        if (ssl)
                ret = SSL_write (ssl, send_buffer + send_sent, size);
        else
#endif
                ret = send (sock, send_buffer + send_sent, size, 0);


        if (! check_read_write_ret (ret, _("Error occured during sending data.")))
                return;

        if (progress_desc && progress == -1){
                progress = progress_setup (send_size, "%s",
                                           progress_desc->str);
        }

        if (progress != -1)
                progress_advance (progress, ret);
        
        total_sent += ret;
        send_sent  += ret;

        if (send_sent == send_size){
                send_buffer = NULL;
                send_size   = 0;
                send_sent   = 0;
                status      = NET_READY;

                cmd_del_writefd_handler (sock);

                /* It is absolutely crucial, that this call is the last
                   thing to do in this function.  It is because send_fun
                   may (implicitly) modify some important data structures.
                   It may remove some readfd handlers (including this one),
                   or add a new one. It may even destroy the enet object. */
                send_fun ();
        }
}



void
Net::send_fun (void)
{
        status = NET_READING_DATA;

        if (progress != -1)
                progress_close (progress);

        if (bytes_expected != 0)
                progress = progress_setup (bytes_expected, "%s",
                                           progress_desc->str);

        cmd_add_readfd_handler (sock, read_handler);
}



static void
read_handler (int fd)
{
        Net *net = Net::find_by_sock (fd);

        if (net){
                net->data_reader ();
                return;
        }
        
        error_ (0, "%s", _("No descriptor found: please submit a bug"));
        cmd_del_readfd_handler (fd);
}



static void
write_handler (int fd)
{
        Net *net = Net::find_by_sock (fd);

        if (net){
                net->data_sender ();
                return;
        }

        error_ (0, "%s", _("No descriptor found: please submit a bug"));
        cmd_del_readfd_handler (fd);
}


/****************************************************************************
 *    INTERFACE FUNCTIONS
 ****************************************************************************/



void
net_init (void)
{
        signal (SIGPIPE, SIG_IGN);
        
#ifdef OPENSSL_SUPPORT
        SSL_load_error_strings ();
        SSL_library_init ();
        ssl_ctx = SSL_CTX_new (SSLv23_method ());
#endif
}




void
net_free_resources (void)
{
#ifdef OPENSSL_SUPPORT
        if (ssl_ctx)
                SSL_CTX_free (ssl_ctx);
#endif
}


/****************************************************************************
 *    INTERFACE CLASS BODIES
 ****************************************************************************/

Net::Net (void)
{
        sock           = -1;
        ssl            = NULL;

        progress       = -1;
        progress_desc  = NULL;
        bytes_expected = 0;
        bytes_received = 0;

        total_sent     = 0;
        total_recv     = 0;
        time_start     = time (NULL);

        is_compiled    = false;
        read_size      = 0;
        read_fill      = 0;
        read_buffer    = NULL;

        send_size      = 0;
        send_sent      = 0;
        send_buffer    = NULL;
        
        status         = NET_DOWN;
        
        server_name    = NULL;
        server_port    = 0;
        local_port     = 0;

        memset (& local_addr,  '\0', sizeof (local_addr));
        memset (& server_addr, '\0', sizeof (server_addr));

        Net::add (this);
}



Net::~Net (void)
{
        close (false);
        
        Net::remove (this);
}


bool
Net::open (const char *hostname, unsigned short port, bool secure)
{
        int p;
        
        server_name = xstrdup (hostname);
        server_port = htons (port);

        if (sigsetjmp (discard_connection, 1)){
                if (p != -1)
                        progress_close (p);
                error_ (0, _("Connection to %s has been terminated."),
                        server_name);
                return false;
        }

        p     = progress_setup (1, _("resolving host %s..."), hostname);
        errno = 0;
        prev_handler = signal (SIGINT, connection_break);

        if (! resolve_host ()){
                if (p != -1)
                        progress_close (p);
                signal (SIGINT, prev_handler);
                return false;
        }
        
        progress_change_desc (p, _("connecting to %s..."),
                              inet_ntoa (server_addr));

        if (! make_connection (secure)){
                if (p != -1)
                        progress_close (p);
                signal (SIGINT, prev_handler);
                return false;
        }
        signal (SIGINT, prev_handler);
        progress_close (p);

        return true;
}



void
Net::close (bool callback)
{
        shutdown_connection ();

        if (server_name)
                xfree (server_name);
        server_name = NULL;

        if (is_compiled)
                regfree (& terminator);
        is_compiled = false;

        if (read_buffer)
                xfree (read_buffer);
        read_buffer = NULL;

        if (progress != -1)
                progress_close (progress);
        progress = -1;

        if (progress_desc)
                str_destroy (progress_desc);
        progress_desc = NULL;

#ifdef OPENSSL_SUPPORT
        if (ssl)
                SSL_free (ssl);
#endif
        ssl = NULL;

        if (sock != -1){
                cmd_del_readfd_handler (sock);
                cmd_del_writefd_handler (sock);
        }

        sock           = -1;
        bytes_expected = 0;
        bytes_received = 0;

        read_fill      = 0;
        send_sent      = 0;

        status         = NET_DOWN;
        
        if (callback)
                on_shutdown ();
}



bool
Net::is_connected (void)
{
        return status >= NET_READY;
}



void
Net::net_recv (const char *re)
{
        int ret;

        if (status != NET_READY)
                return;
        
        ret = regcomp (& terminator, re, REG_ICASE | REG_EXTENDED);
        if (ret){
                error_regex (ret, & terminator, re);
                regfree (& terminator);
                is_compiled = 0;
                return;
        }

        is_compiled = 1;
        status      = NET_READING_DATA;

        cmd_add_readfd_handler (sock, read_handler);
}



void
Net::net_send (char *buf, int size)
{
        if (status != NET_READY)
                return;
        
        send_size   = size;
        send_buffer = buf;
        status      = NET_WRITING_DATA;

        cmd_add_writefd_handler (sock, write_handler);
}



void
Net::expect (int size, char *desc, ...)
{
        va_list ap;
        
        if (status != NET_READY)
                return;

        if (progress_desc)
                str_clear (progress_desc);
        else
                progress_desc = str_create ();

        bytes_expected = size;
        bytes_received = 0;

        va_start (ap, desc);
        str_vsprintf (progress_desc, desc, ap);
        va_end (ap);
}



void
Net::send_progress (char *desc, ...)
{
        va_list ap;

        if (status != NET_READY)
                return;

        if (progress_desc)
                str_clear (progress_desc);
        else
                progress_desc = str_create ();

        va_start (ap, desc);
        str_vsprintf (progress_desc, desc, ap);
        va_end (ap);
}



void
Net::combo (char *buf, int size, const char *re)
{
        int ret;
        
        if (status != NET_READY)
                return;

        if (is_compiled){
                regfree (& terminator);
                is_compiled = 0;
        }
        
        ret = regcomp (& terminator, re,
                       REG_ICASE | REG_EXTENDED | REG_NEWLINE);
        if (ret){
                error_regex (ret, & terminator, re);
                regfree (& terminator);
                is_compiled = 0;
                return;
        }

        is_compiled = 1;

        net_send (buf, size);
}


void
Net::add (Net *net)
{
        struct nlist *entry = (struct nlist*) xmalloc (sizeof (struct nlist));

        entry->next = Net::nlist;
        entry->net  = net;

        nlist = entry;
}


void
Net::remove (Net *net)
{
        struct nlist *prev = Net::nlist;
        struct nlist *seek = Net::nlist;
        
        if (seek == NULL)
                return;

        while (seek->net != net){
                prev = seek;
                seek = seek->next;
        }

        prev->next = seek->next;

        xfree (seek);
}


Net *
Net::find_by_sock (int fd)
{
        struct nlist *l;

        for (l = nlist; l; l = l->next)
                if (l->net->sock == fd)
                        return l->net;

        return NULL;
}

/****************************************************************************
 *
 *    END MODULE networking.cc
 *
 ****************************************************************************/


syntax highlighted by Code2HTML, v. 0.9.1