/*
  * Copyright (C) 2003  Mihai RUSU (dizzy@roedu.net)
  *
  * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

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

#include <stdio.h>
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#ifdef HAVE_SYS_SOCKET_H
# include <sys/socket.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif
#ifdef HAVE_SYS_UN_H
# include <sys/un.h>
#endif
#ifdef HAVE_NETINET_IN_H
# include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif
#ifdef HAVE_SIGNAL_H
# include <signal.h>
#endif
#ifdef HAVE_ERRNO_H
# include <errno.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_SYS_PARAM_H
# include <sys/param.h>
#endif

#include "list.h"
#include "unix2tcp.h"
#include "server.h"

typedef struct {
   char *data;
   int len;
} t_packet;

typedef struct {
   int sock;
   t_list *outqueue;
} t_mysocket;

typedef struct {
   t_mysocket in, out;
   int inprogress;
} t_connection;

/* Global Variable Definitions */

static int listensock;
static struct sockaddr_in raddr;
static fd_set trfds, twfds;
static int maxsocket = 0, findmax;
static t_list *connlist = NULL;

static void packet_destroy(t_packet *);

/* Connection handling routines */
static int conn_new(int);
static int conn_establish(t_connection *);
static void conn_destroy(t_connection *);

static int socket_nonblock(int);
static int init_socket(void);
static void close_socket(void);

static int server_read(t_mysocket *, t_mysocket *);
static int server_write(t_mysocket *);
static void mainloop(void);

void server_process(void)
{
    mylog("STARTUP\n");

    if (init_socket() < 0) {
	mylog("Error in init_socket()!\n");
	return;
    }

    mainloop();

    close_socket();
    mylog("SHUTDOWN\n");
}

static int init_socket(void)
{
    struct sockaddr_un unixaddr;

    if (unixpath == NULL || raddrs == NULL || rports == NULL) return -1;

    if (inet_aton(raddrs, &raddr.sin_addr) == 0) {
	mylog("Error converting remote IP address (errno: %d str: '%s')", errno, strerror(errno));
	return -1;
    }

    if (strlen(unixpath) >= MAXPATHLEN || strlen(unixpath) >= sizeof(unixaddr.sun_path)) {
	mylog("Error : unix socket len too long!\n");
	return -1;
    }

    raddr.sin_family = AF_INET;
    raddr.sin_port = htons(atoi(rports));

    listensock = socket(PF_UNIX, SOCK_STREAM, 0);
    if (listensock < 0) {
	mylog("Error on socket() (errno: %d str: '%s'\n", errno, strerror(errno));
	return -1;
    }

    if (socket_nonblock(listensock) < 0) {
	mylog("Error setting O_NONBLOCK (errno: %d str: '%s')\n", errno, strerror(errno));
        close(listensock);
	return -1;
    }

    memset(&unixaddr, 0, sizeof(unixaddr));

    unixaddr.sun_family = AF_UNIX;
    strcpy(unixaddr.sun_path, unixpath);

    if (bind(listensock, (struct sockaddr *)&unixaddr, sizeof(unixaddr))) {
	mylog("Error on bind() (errno: %d str: '%s')\n", errno, strerror(errno));
        close(listensock);
        return -1;
    }

    if (chmod(unixpath, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH) < 0) {
	mylog("Error on chmod() (errno: %d str: '%s')\n", errno, strerror(errno));
        close(listensock);
        return -1;
    }

    if (listen(listensock, 5)) {
	mylog("Error on listen() (errno: %d str: '%s')\n", errno, strerror(errno));
        close(listensock);
        unlink(unixpath);
        return -1;
    }

    FD_ZERO(&trfds); FD_ZERO(&twfds);
    FD_SET(listensock, &trfds);

    if (maxsocket < listensock) maxsocket = listensock;

    findmax = 100;

    return 0;
}

static void close_socket(void)
{
    t_list_elem *elem;

    /* FIXME: close all incoming/outgoing connections */
    if (listensock) close(listensock);
    unlink(unixpath);
    if (connlist) {
	for(elem = LIST_FIRST(connlist); elem ; elem = LIST_NEXT(elem)) {
	    t_connection *c = elem_get_data(elem);

	    if (c) conn_destroy(c);
	    else mylog("close_socket : found NULL entry in conn list\n");
	}
	list_free(connlist);
	connlist = NULL;
    }
    FD_ZERO(&trfds); FD_ZERO(&twfds);
}

static void mainloop(void)
{
    fd_set rfds, wfds;
    int res;
    t_list_elem *elem, *next;

    if (connlist == NULL && (connlist = list_init()) == NULL) return;
    while(!quitasap) {
	memmove(&rfds, &trfds, sizeof(fd_set));
	memmove(&wfds, &twfds, sizeof(fd_set));
	res = select(maxsocket + 1, &rfds, &wfds, NULL, NULL);
	if (res == 0)
	    mylog("mainloop() : select returned with 0 fds!\n");
        else if (res < 0)
	    switch(errno) {
		case EINTR: /* got signal */
		    break;
		default:
		    mylog("mainloop() : got error in select()(%s)\n", strerror(errno));
	    }
	else {
	    if (FD_ISSET(listensock, &rfds)) {
		int newsock;

		newsock = accept(listensock, NULL, NULL);
		if (newsock < 0)
	    	    switch(errno) {
	    		case EAGAIN:
			    mylog("mainloop() : select returned rdy but accept() would block\n");
			    break;
	    		default:
			    mylog("mainloop() : got error in accept()(%s)\n", strerror(errno));
	    	    }
		else if (conn_new(newsock) < 0)
	        /* could not creat the proxy connection, close the source */
	        close(newsock);
	    }

	    for(elem = LIST_FIRST(connlist); elem ; elem = next) {
		t_connection *c = elem_get_data(elem);

		next = LIST_NEXT(elem);
		if (c) {
		    res = 0;
	    	    if (FD_ISSET(c->in.sock, &rfds)) res = server_read(&c->in, &c->out);
	    	    if (res == 0 && FD_ISSET(c->in.sock, &wfds)) res = server_write(&c->in);
	    	    if (res == 0 && FD_ISSET(c->out.sock, &rfds)) res = server_read(&c->out, &c->in);
	    	    if (res == 0 && FD_ISSET(c->out.sock, &wfds)) {
			if (c->inprogress) res = conn_establish(c);
			else res = server_write(&c->out);
	    	    }

		    if (res < 0) quitasap = 1;
	    	    else if (res > 0) {
			conn_destroy(c);
			list_delete_by_elem(connlist, elem);
	    	    }
		} else mylog("mainloop() : got NULL entry in in_conn list\n");
	    }
	}
    }
}

static int server_read(t_mysocket *in, t_mysocket *out)
{
    static char buffer[4096];
    t_packet *packet;
    int res, len;

    if (in == NULL || out == NULL) {
	mylog("server_read : Got NULL in/out sockets\n");
        return -1;
    }

    res = read(in->sock, buffer, sizeof(buffer));
    if (res < 0) {
	switch(errno) {
    	    case EAGAIN: /* no data available */
    	    case EINTR: /* call was interupted by a signel before any data was read */
		return 0;
    	    case ECONNRESET:
		return 1;
    	    default:
		mylog("server_read : got error in read() (%s)\n", strerror(errno));
		return -1;
        }
    }
    if (res == 0) return 1; /* connection closed by peer */

    res = write(out->sock, buffer, len = res);

    if (res == len) return 0;
    if (res < 0) {
	switch(errno) {
    	    case EPIPE: /* connection closed */
    	    case ECONNRESET:
		return 1;
    	    case EAGAIN: /* would block */
    	    case EINTR: /* interupted in write() */
		break;
    	    default: 
		mylog("server_read : error in write() (%s)\n", strerror(errno));
		return -1;
        }
    } else if (res && res < len) { /* sent a part of original data */
	len -= res;
        memmove(buffer, buffer + res, len);
    }

    /* could not send read data, so we queue it for later send */
    if (out->outqueue == NULL && (out->outqueue = list_init()) == NULL) {
	mylog("server_read : could not init the outqueue\n");
        return -1;
    }

    if ((packet = malloc(sizeof(t_packet))) == NULL) {
	mylog("server_read : could not allocate new packet\n");
        return -1;
    }

    if ((packet->data = malloc(len)) == NULL) {
	mylog("server_read : could not allocate for data\n");
        free(packet);
        return -1;
    }

    memmove(packet->data, buffer, len);
    packet->len = len;

    if (list_append_data(out->outqueue, packet) < 0) {
	mylog("server_read : could not append packet to out\n");
        free(packet->data);
        free(packet);
        return -1;
    }

    FD_SET(out->sock, &twfds); /* next select() will need to check for writing */

    return 0;
}

static int server_write(t_mysocket *con)
{
    int res;
    t_list_elem *elem;
    t_packet *packet;

    if (con == NULL) {
	mylog("server_write : got NULL con\n");
        return -1;
    }

    if (con->outqueue == NULL) {
	mylog("server_write : got NULL outqueue\n");
        return -1;
    }

    elem = LIST_FIRST(con->outqueue);
    if (elem == NULL) {
	mylog("server_write : outqueue is empty\n");
        return -1;
    }

    packet = elem_get_data(elem);
    if (packet == NULL || packet->data == NULL) {
	mylog("server_write : found NULL packet in outqueue\n");
        return -1;
    }

    res = write(con->sock, packet->data, packet->len);

    if (res < 0) {
	switch(errno) {
    	    case EPIPE: /* connection closed */
    	    case ECONNRESET:
		return 1;
    	    case EAGAIN: /* would block */
    	    case EINTR: /* interupted in write() */
		return 0;
    	    default: 
		mylog("server_write : error in write() (%s)\n", strerror(errno));
		return -1;
        }
    }

    if (!res) return 0; /* nothing written */
    if (res < packet->len) { /* we wrote fewer bytes than requested */
	memmove(packet->data, (char *)packet->data + res, packet->len - res);
        packet->len -= res;
        return 0;
    }

    packet_destroy(packet);
    if (list_delete_by_elem(con->outqueue, elem) < 0) {
	mylog("server_write : could not remove sent packet from outqueue\n");
        return -1;
    }

    /* outqueue is empty so next select() wont need to check for write */
    if (list_get_size(con->outqueue) == 0) FD_CLR(con->sock, &twfds); 

    return 0;
}

static int conn_new(int newsock)
{
    t_connection *c;
    int outsock;

    if (connlist == NULL) return -1;
    if (newsock < 1) {
	mylog("conn_new() : got invalid socket\n");
        return -1;
    }

    c = malloc(sizeof(t_connection));

    if (c == NULL) {
	mylog("conn_new() : could not allocate for new connections\n");
        return -1;
    }

    outsock = socket(PF_INET, SOCK_STREAM, 0);
    if (outsock < 1) {
	mylog("conn_new() : could not create new socket\n");
        free(c);
        return -1;
    }
   
    if (socket_nonblock(outsock) < 0) {
	mylog("conn_new() : could not set O_NONBLOCK on socket\n");
        free(c);
        close(outsock);
        return -1;
    }

    if (connect(outsock, (struct sockaddr *) &raddr, sizeof(raddr))) {
	switch(errno) {
    	    case EINPROGRESS: /* note a in progress connection */
		c->inprogress = 1;
		break;
    	    default:
		mylog("conn_new() : could not connect()(%s)\n", strerror(errno));
		close(outsock);
		free(c);
		return -1;
        }
    } else c->inprogress = 0;

    c->in.sock = newsock;
    c->in.outqueue = NULL;

    c->out.sock = outsock;
    c->out.outqueue = NULL;

    if (list_append_data(connlist, c) < 0) {
	mylog("conn_new() : could not append new connection to list\n");
        close(outsock);
        free(c);
        return -1;
    }

    if (maxsocket < c->in.sock) maxsocket = c->in.sock;
    if (maxsocket < c->out.sock) maxsocket = c->out.sock;

    if (!c->inprogress) { /* connection fully established */
	FD_SET(c->in.sock, &trfds);
        FD_SET(c->out.sock, &trfds);
    } else FD_SET(c->out.sock, &twfds); /* connection in progress, wait for write */

    return 0;
}

static int conn_establish(t_connection *c)
{
    int error;
    socklen_t len;

    if (c == NULL) {
	mylog("conn_establish() : found NULL connection\n");
        return -1;
    }

    if (!c->inprogress || c->out.sock <= 0) {
        mylog("conn_establish() : got invalid connection\n");
        return -1;
    }

    len = sizeof(error);
    if (getsockopt(c->out.sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
	mylog("conn_establish() : could not getsockopt()\n");
        return -1;
    }

    if (error) /* could not establish remote connection */
	return 1; /* close this connection */

    c->inprogress = 0; /* connection established */
    FD_CLR(c->out.sock, &twfds);
    FD_SET(c->in.sock, &trfds); FD_SET(c->out.sock, &trfds);

    return 0;
}

static void conn_destroy(t_connection *c)
{
    t_list_elem *elem;
    t_packet *packet;

    if (c == NULL) return;
    close(c->in.sock);
    close(c->out.sock);

    FD_CLR(c->in.sock, &trfds); FD_CLR(c->out.sock, &trfds);
    FD_CLR(c->in.sock, &twfds); FD_CLR(c->out.sock, &twfds);

    if (--findmax == 0) {
	t_connection *con;

        maxsocket = listensock;
        for(elem = LIST_FIRST(connlist); elem; elem = LIST_NEXT(elem)) {
	    con = elem_get_data(elem);

	    if (con && con != c) {
		if (con->in.sock > maxsocket) maxsocket = con->in.sock;
		if (con->out.sock > maxsocket) maxsocket = con->out.sock;
	    }
	}
	findmax = 100;
    }

    if (c->in.outqueue) {
	for(elem = LIST_FIRST(c->in.outqueue); elem; elem = LIST_NEXT(elem)) {
	    packet = elem_get_data(elem);

	    if (packet) packet_destroy(packet);
	    else mylog("conn_destroy : found NULL entry in outqueue\n");
        }
	list_free(c->in.outqueue);
    }
    if (c->out.outqueue) {
	for(elem = LIST_FIRST(c->out.outqueue); elem; elem = LIST_NEXT(elem)) {
	    packet = elem_get_data(elem);

	    if (packet) packet_destroy(packet);
	    else mylog("conn_destroy : found NULL entry in outqueue\n");
        }
	list_free(c->out.outqueue);
    }
   
    free(c);
}

static void packet_destroy(t_packet *packet)
{
    if (packet == NULL) return;

    if (packet->data) free(packet->data);
    free(packet);
}

static int socket_nonblock(int fd)
{
    int flags;

    flags = fcntl(fd, F_GETFL);
    if (flags < 0) return -1;

    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) < 0) return -1;
    return 0;
}
