/*
 * Copyright (c) 2004-2009, Luiz Otavio O Souza <loos.br@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

static const char rcsid[] = "$Id: sb.c 112 2009-03-15 17:30:28Z loos-br $";

#include <sys/types.h>
#include <sys/socket.h>

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "sb.h"
#include "sql.h"
#include "net-io.h"
#include "return.h"
#include "contacts.h"
#include "protocol.h"
#include "array_cmd.h"
#include "check-cmd.h"
#include "msn-proxy.h"

void
sb_free_xfr(struct sb_ *sb) {
    if (sb->xfr) {
	str_free(sb->xfr);
	free(sb->xfr);
	sb->xfr = (string *)0;
    }
}

struct sb_ *
sb_alloc(struct user_ *user) {
 struct sb_		*sb;

    sb = (struct sb_ *)malloc(sizeof(struct sb_));
    if (sb == NULL) die_nomem();
    memset(sb, 0, sizeof(struct sb_));

    sb->listen.ev_fd = -1;
    sb->user	     = user;
    sb->closed	     = NO;
    sb->warned	     = NO;

    /* create a new sb entry on sql */
    sb->id = sql_insert_sb(user);
    if (sb->id <= 0) {
	free (sb);
	return(NULL);
    }

    /* initialize users list */
    SLIST_INIT(&sb->sb_users);

    /* insert this new sb on user sb_list */
    LIST_INSERT_HEAD(&user->sbs, sb, sb__);

    return(sb);
}

struct sb_user_ *
sb_user_alloc(struct sb_ *sb, string *email) {
 struct sb_user_	*sb_user;

    sb_user = (struct sb_user_ *)malloc(sizeof(struct sb_user_));
    if (sb_user == NULL) die_nomem();

    memset(sb_user, 0, sizeof(struct sb_user_));
    SLIST_INSERT_HEAD(&sb->sb_users, sb_user, sb_user__);
    if (email->len > 0 && str_copy(&sb_user->email, email->s, email->len) == 0)
	die_nomem();
    return(sb_user);
}

struct sb_user_ *
sb_user_free(struct sb_ *sb, struct sb_user_ *sb_user) {
    SLIST_REMOVE(&sb->sb_users, sb_user, sb_user_, sb_user__);
    str_free(&sb_user->email);
    free(sb_user);
    return(NULL);
}

void
sb_connect(const int listenfd, short event, void *p) {
 struct	sockaddr	client_sa;
 struct sb_		*sb = p;
 socklen_t		client_sa_len;
 log_			*log = &config.log;
 int			client_fd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: sb proxy connection timeout\n");
	sb = sb_disconnect(sb);
	goto end;
    }

    /* prepare client sockaddr */
    client_sa_len = sizeof(struct sockaddr);
    memset(&client_sa, 0, client_sa_len);

    /* accept connection */
    client_fd = accept(listenfd, &client_sa, &client_sa_len);
    if (client_fd < 0) {
	log->debug("debug: unable to accept new sb client connection\n");
	sb = sb_disconnect(sb);
	goto end;
    }

    /* set socket options */
    if (set_options(client_fd) == -1 || set_nonblock(client_fd) == -1) {
	log->debug("debug: unable to set socket options on sb client fd\n");
	sb = sb_disconnect(sb);
	goto end;
    }

    /* attach server */
    sb->server = server_connect2(sb->xfr, sb_server_read, sb_server_write, sb);
    if (sb->server == (server_ *)0) {
	log->debug("debug: fail to connect to sb server\n");
	sb = sb_disconnect(sb);
	goto end;
    }

    /* free xfr */
    sb_free_xfr(sb);

    /* attach client */
    sb->client = client_alloc(client_fd, sb_client_read, sb_client_write, sb);

    /* reset queue events */
    if (EVENT_FD((&sb->listen)) != -1) {
	event_del(&sb->listen);
	sb->listen.ev_fd = -1;
    }

end:
    /* close listenfd */
    while (close(listenfd) != 0 && errno == EINTR);
}

struct sb_ *
sb_disconnect(struct sb_ *sb) {
 struct sb_user_	*sb_user;
 struct user_		*user = sb->user;

    /* log stop */
    if (sb->closed == NO)
	sql_log_stop(sb, &user->email, &user->dn);

    /* remove sb session from sql */
    sql_remove_sb(sb->id);

    /* close connection to NS */
    sb->server = server_disconnect(sb->server);

    /* close connection to NS client */
    sb->client = client_disconnect(sb->client);

    /* reset queue events */
    if (EVENT_FD((&sb->listen)) != -1) {
	event_del(&sb->listen);
	sb->listen.ev_fd = -1;
    }

    /* free xfr if exist */
    sb_free_xfr(sb);

    /* free start if exist */
    str_free(&sb->start);

    /* free sb->sb_users */
    while (!SLIST_EMPTY(&sb->sb_users)) {
	sb_user = SLIST_FIRST(&sb->sb_users);
	sb_user = sb_user_free(sb, sb_user);
    }

    LIST_REMOVE(sb, sb__);

    free(sb);

    return(NULL);
}

void
sb_disconnect_all(struct user_ *user) {
 struct sb_		*sb;

    while (!LIST_EMPTY(&user->sbs)) {
	sb = LIST_FIRST(&user->sbs);
	sb_disconnect(sb);
    }
}

void
sb_client_write(const int evfd, short event, void *p) {
 log_			*log      = &config.log;
 struct sb_		*sb       = p;
 struct user_		*user     = sb->user;
 client_		*client   = sb->client;
 server_		*server   = sb->server;
 client_		*nsclient = user->ns.client;
 command		*cmd      = server->commands.cmd;
 const char		*to       = "sb client";

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: sb client write timeout\n");
	sb = sb_disconnect(sb);
	return;
    }

    if (!HAS_CMD(server))
	return;

    /* hook for send commands */
    if (check_sb_cmd(sb, cmd, SERVER_SB_CMD) == RFAIL) {

	log->debug("debug: sb server command not accepted\n");
	sb = sb_disconnect(sb);
	return;
    }

    /* flush the server command queue */
    switch (send_command(client->fd, cmd, &user->email, to)) {

	case AGAIN:
	    client_sched_write(client);
	    return;

	case RFAIL:

	    log->debug("debug: fail to send sb client command\n");
	    sb = sb_disconnect(sb);
	    return;

	default: break;
    }

    /* hook for post send commands */
    switch (check_sb_cmd(sb, cmd, SERVER_SB_POST_CMD)) {

	case RFAIL:
	    log->debug("debug: fail to run sb server post command\n");
	    /* fallthrough */

	case DISCONNECT:
	    sb = sb_disconnect(sb);
	    /* fallthrough */

	case RETURN:
	    return;
    }

    /* command >> 1 */
    shift_server_commands(server, cmd);
    free_command(cmd);

    client_sched_read(client);
    client_sched_read(nsclient);

    if (!HAS_CMD(server))
	return;

    client_sched_write(client);
}

void sb_client_read(const int evfd, short event, void *p) {
 struct sb_		*sb	  = p;
 log_			*log	  = &config.log;
 client_		*client   = sb->client;
 client_		*nsclient = sb->user->ns.client;

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: sb client read timeout\n");
	sb = sb_disconnect(sb);
	return;
    }

    /* read user command */
    switch (read_client_command(client, server_sched_write, sb->server)) {

	case AGAIN:
	    client_sched_read(client);
	    client_sched_read(nsclient);
	    return;

	case END:
	    log->debug("debug: connection closed by sb client\n");
	    sb = sb_disconnect(sb);
	    return;

	case RFAIL:
	    log->debug("debug: fail to read sb client command\n");
	    sb = sb_disconnect(sb);
	    return;
    }

    client_sched_read(client);
    client_sched_read(nsclient);
}

void
sb_server_read(int evfd, short event, void *p) {
 struct sb_		*sb	  = p;
 log_			*log	  = &config.log;
 server_		*server   = sb->server;
 server_		*nsserver = sb->user->ns.server;

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: sb server read timeout\n");
	sb = sb_disconnect(sb);
        return;
    }

    switch (read_server_command(server, client_sched_write, sb->client)) {

	case AGAIN:
	    server_sched_read(server);
	    server_sched_read(nsserver);
	    return;

	case END:
            /* connection reset by peer */

	    if (HAS_CMD(server)) {

		/* close connection to ns server - wait flush of command queue */
		server_close(server);

		log->debug("debug: closed connection to ns server. flushing queue.\n");
		return;
            }

	    log->debug("debug: connection closed by sb server\n");
	    sb = sb_disconnect(sb);
	    return;

	case RFAIL:
	    log->debug("debug: fail to read sb server command\n");
	    sb = sb_disconnect(sb);
	    return;
    }

    server_sched_read(server);		/* schedule another read */
    server_sched_read(nsserver);	/* reset ns timeout */
}

void
sb_server_write(int evfd, short event, void *p) {
 log_			*log	= &config.log;
 struct sb_		*sb	= p;
 struct user_		*user	= sb->user;
 client_		*client = sb->client;
 server_		*server = sb->server;
 command		*cmd	= client->commands.cmd;
 const char		*to	= "sb server";

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: sb server write timeout\n");
	sb = sb_disconnect(sb);
	return;
    }

    if (!HAS_CMD(client))
	return;

    /* hook for send commands */
    if (check_sb_cmd(sb, cmd, CLIENT_SB_CMD) == RFAIL) {
	log->debug("debug: client sb command not accepted\n");
	sb = sb_disconnect(sb);
	return;
    }

    switch (send_command(server->fd, cmd, &user->email, to)) {

	case AGAIN:
	    server_sched_write(server);
	    return;

	case RFAIL:

	    log->debug("debug: fail to send sb server command\n");
	    sb = sb_disconnect(sb);
	    return;

	default: break;
    }

    /* hook for post send commands */
    switch (check_sb_cmd(sb, cmd, CLIENT_SB_POST_CMD)) {

	case RFAIL:
	    log->debug("debug: client sb post command not accepted\n");
	    /* fallthrough */

	case DISCONNECT:
	    sb = sb_disconnect(sb);
	    /* fallthrough */

	case RETURN:
	    return;
    }

    /* command >> 1 */
    shift_client_commands(client, cmd);
    free_command(cmd);

    if (!HAS_CMD(client))
	return;

    server_sched_write(server);
}

struct sb_ *
sb_proxy(struct user_ *user, string **arg) {
 struct sb_		*sb;
 string			*port;
 int			listenfd;

    /* alloc new sb */
    sb = sb_alloc(user);
    if (sb == NULL)
	return(NULL);

    port = bind_free_port(config.port_min, config.port_max,
			&config.defaults.internal_host, &listenfd);
    if (port == (string *)0) {

	/* cannot allocate any port */
	sb = sb_disconnect(sb);
	return(NULL);
    }

    /* save old xfr host */
    sb->xfr = *arg;

    /* setup new xfr host */
    *arg = port;

    /* event set */
    event_set(&sb->listen, listenfd, EV_READ, sb_connect, sb);
    event_add(&sb->listen, &config.timeout_listen);

    return(sb);
}

int
xfr_sb_proxy(struct user_ *user, command *cmd, int args) {
 struct sb_		*sb;
 string			**address;

    if (cmd->args_len < 3)
	return(RFAIL);

    address = &cmd->args[2];

    /* proxy sb connection */
    sb = sb_proxy(user, address);
    if (sb == NULL)
	return(RFAIL);

    /* save start email */
    if (msn_decode(&user->email, &sb->start) == RFAIL)
	return(RFAIL);

    if (sql_log_start(sb->id, &user->email, &user->dn) == RFAIL)
	return(RFAIL);

    return(ROK);
}

int
rng_sb_proxy(struct user_ *user, command *cmd, int args) {
 struct contact_	*contact;
 struct sb_		*sb;
 string			dn;
 string			*dn_;
 string			*email;
 string			**address;

    if (cmd->args_len < 6)
	return(RFAIL);

    address = &cmd->args[1];
    if ((email = get_arg(cmd, 4)) == NULL ||
	(dn_ = get_arg(cmd, 5)) == NULL ||
	*address == NULL) {

	return(RFAIL);
    }

    /* user is allowed to chat with us ? */
    contact = contact_find(user, email);
    if (contact && (contact->deny & CONTACT_DENY))
	return(RETURN);

    /* proxy sb connection */
    sb = sb_proxy(user, address);
    if (sb == NULL)
	return(RFAIL);

    /* save start email */
    if (msn_decode(email, &sb->start) == RFAIL)
	return(RFAIL);

    str_zero(&dn);
    if (msn_decode(dn_, &dn) == RFAIL)
	return(RFAIL);

    if (sql_log_start(sb->id, email, &dn) == RFAIL) {
	str_free(&dn);
	return(RFAIL);
    }
    str_free(&dn);

    if (sql_log_join(sb->id, &user->email, &user->dn) == RFAIL)
	return(RFAIL);

    return(ROK);
}

int
msn_sb_joi(struct sb_ *sb, command *cmd, int args) {
 struct sb_user_	*sb_user;
 string			*dn_   = cmd->args[1];
 string			*email = cmd->args[0];
 string			dn;

    if (cmd->args_len < 2)
	return(RFAIL);

    /*
     * save user email - this may be a multiuser chat, so this information
     * is needed when we receive a BYE command from user
     */
    sb_user = sb_user_alloc(sb, email);

    /* decode display name */
    str_zero(&dn);
    if (msn_decode(dn_, &dn) == RFAIL)
	return(RFAIL);

    if (sql_log_join(sb->id, email, &dn) == RFAIL) {
	str_free(&dn);
	return(RFAIL);
    }

    str_free(&dn);
    return(ROK);
}

int
msn_sb_post_out(struct sb_ *sb, command *cmd, int args) {
    return(DISCONNECT);
}

int
sb_iro(struct sb_ *sb, command *cmd, int args) {
 struct sb_user_	*sb_user;
 string			*email;
 string			*dn_;
 string			dn;
 log_			*log = &config.log;

    str_zero(&dn);

    if (cmd->args_len < 5) {
	log->debug("debug: ARGS_LEN != IRO_LEN\n");
	print_command(cmd);
        return(RFAIL);
    }

    /* to access any pointer inside a command, check args_len first */
    dn_	  = cmd->args[4];
    email = cmd->args[3];

    /*
     * save user email - this may be a multiuser chat, so this information
     * is needed when we receive a BYE command from user
     */
    sb_user = sb_user_alloc(sb, email);

    if (msn_decode(dn_, &dn) == RFAIL) {

	log->debug("debug: fail to decode SB_IRO\n");
	str_free(&dn);
	return(RFAIL);
    }

    if (sb->start.len > 0 && email->len > 0 &&
	    strcmp((char *)sb->start.s, (char *)email->s) != 0) {

	/* principal has not started the connection - log join */
	if (sql_log_join(sb->id, email, &dn) == RFAIL) {
	    str_free(&dn);
	    return(RFAIL);
	}
    }

    str_free(&dn);
    return(ROK);
}

int
msn_sb_post_bye(struct sb_ *sb, command *cmd, int args) {
 struct sb_user_	*sb_user;
 struct user_		*user = sb->user;

    if (cmd->args_len < 1)
	return(RFAIL);

    SLIST_FOREACH(sb_user, &sb->sb_users, sb_user__) {

	if (strcmp((char *)sb_user->email.s, (char *)cmd->args[0]->s) == 0) {

	    /* log stop */
	    if (strcmp((char *)user->email.s, (char *)sb_user->email.s) == 0)
		sql_log_stop(sb, &user->email, &user->dn);
	    else
		sql_log_stop2(sb, &user->email, &sb_user->email);

	    sb_user = sb_user_free(sb, sb_user);
	    break;
	}
    }

    /* last user on this sb connection ? */
    if (SLIST_EMPTY(&sb->sb_users)) {
	sb->closed = YES;
	return(DISCONNECT);
    }

    return(ROK);
}

int
msn_sb_cal(struct sb_ *sb, command *cmd, int args) {
 struct contact_	*contact;
 struct user_		*user = sb->user;
 string                 *email;

    if (cmd->args_len < 2)
	return(RFAIL);

    if ((email = get_arg(cmd, 1)) == NULL)
	return(RFAIL);

    /* user is allowed to chat with us ? */
    contact = contact_find(user, email);
    if (contact && (contact->deny & CONTACT_DENY))
	return(RETURN);

    return(ROK);
}

