/*
 * Copyright (C) Tildeslash Ltd. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3.
 *
 * 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 Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU Affero General Public License in all respects
 * for all of the code used other than OpenSSL.  
 */

#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

#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif

#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif

#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif


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

// libmonit
#include "exceptions/assert.h"
#include "util/Str.h"



/**
 * Implementation of the socket interface.
 * 
 * @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];
};


/* --------------------------------------------------------------- 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 = (int)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;

}


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


Socket_T socket_new(const char *host, int port, int type, int use_ssl, int timeout) {
        Ssl_T ssl = {.use_ssl = use_ssl, .version = SSL_VERSION_AUTO};
        return socket_create_t(host, port, type, ssl, timeout);
}


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 = Str_dup(LOCALHOST);
                } else {
                        S->host = Str_dup(p->hostname);
                }

                if(p->SSL.use_ssl && !socket_switch2ssl(S, p->SSL)) {
                        socket_free(&S);
                        return NULL;
                }

                S->Port = port;
                return S;
        }

        return NULL;
}


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->port = port;
                S->type = proto;
                S->timeout = timeout;
                S->host = Str_dup(host);
                S->connection_type = TYPE_LOCAL;

                if(ssl.use_ssl && !socket_switch2ssl(S, ssl)) {
                        socket_free(&S);
                        return NULL;
                }

                return S;
        }

        return NULL;
}


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->port = port;
        S->socket = socket;
        S->type = SOCK_STREAM;
        S->timeout = NET_TIMEOUT;
        S->host = Str_dup(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;

}


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);
                }
        } else
#endif

                close_socket((*S)->socket);
        FREE((*S)->host);
        FREE(*S);

}


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


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;

}


int socket_is_secure(Socket_T S) {

        ASSERT(S);

        return (S->ssl != NULL);

}


int socket_get_socket(Socket_T S) {

        ASSERT(S);

        return S->socket;

}


int socket_get_type(Socket_T S) {

        ASSERT(S);

        return S->type;

}


void *socket_get_Port(Socket_T S) {

        ASSERT(S);

        return S->Port;

}


int socket_get_remote_port(Socket_T S) {

        ASSERT(S);

        return S->port;

}


const char *socket_get_remote_host(Socket_T S) {

        ASSERT(S);

        return S->host;

}


int socket_get_local_port(Socket_T S) {
        struct sockaddr sock;
        socklen_t len = sizeof(sock);

        ASSERT(S);

        if(getsockname (S->socket, &sock, &len ) == 0)
                return ntohs (((struct sockaddr_in *)&sock)->sin_port);
        return -1;

}


const char *socket_get_local_host(Socket_T S) {
        struct sockaddr sock;
        socklen_t len = sizeof(sock);

        ASSERT(S);

        if(getsockname(S->socket, &sock, &len) == 0) 
                return inet_ntoa(((struct sockaddr_in *)&sock)->sin_addr);
        return NULL;

}


void socket_setError(Socket_T S, const char *error, ...) {
        assert(S);
        assert(error);
        va_list ap;
        va_start(ap, error);
        vsnprintf((char*)S->buffer, RBUFFER_SIZE, error, ap);
        va_end(ap);
}


const char *socket_getError(Socket_T S) {
        assert(S);
        return (const char *)S->buffer;
}


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


int socket_switch2ssl(Socket_T S, Ssl_T ssl)  {

        if(! (S->ssl = new_ssl_connection(ssl.clientpemfile, 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;
}


int socket_print(Socket_T S, const char *m, ...) {
        int n;
        va_list ap;
        char *buf = NULL;

        ASSERT(S);
        ASSERT(m);

        va_start(ap, m);
        buf = Str_vcat(m, ap);
        va_end(ap);

        n = socket_write(S, buf, strlen(buf));
        FREE(buf);

        return n;

}


int socket_write(Socket_T S, void *b, size_t size) {

        ssize_t 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);

}


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++];

}


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  (int)((long)p - (long)b);

}


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)) { // Stop when \0 is read
                *p++ = c;
                if(c == '\n')
                        break;
        }

        *p = 0;

        if(*s)
                return s;

        return NULL;

}


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;

}


int socket_shutdown_write(Socket_T S) {
        ASSERT(S);
        return (shutdown(S->socket, 1) == 0);
}
