/*
 * 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: ns.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 <strings.h>
#include <unistd.h>

#include "sb.h"
#include "ns.h"
#include "net-io.h"
#include "return.h"
#include "protocol.h"
#include "msn-proxy.h"
#include "check-cmd.h"

void
ns_server_read(int evfd, short event, void *p) {
 struct user_		*user   = p;
 client_		*client = user->ns.client;
 server_		*server = user->ns.server;
 log_			*log    = &config.log;

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: ns server read timeout\n");
	user_disconnect(user);
        return;
    }

    switch (read_server_command(server, client_sched_write, client)) {

	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 ns server\n");
	    user_disconnect(user);
	    return;

	case RFAIL:
	    log->debug("debug: fail to read ns server command\n");
	    user_disconnect(user);
	    return;
    }

    server_sched_read(server);
}

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

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: ns server write timeout\n");
	user_disconnect(user);
	return;
    }

    /* check for command */
    if (!HAS_CMD(client) || (client->commands.state & SENDDELAY)) {

	/* not ready */
	return;
    }

    /* check state */
    switch (user->state) {

	case PRE_AUTH:
	case USR_TWN_I:
	case USR_TWN_S:

	    /* check for pre-auth client command */
	    if (check_cmd(user, cmd, CLIENT_PRE_CMD) == RFAIL) {

                /* pre-auth client command fail or not found */
		log->debug("debug: ns client pre command not accepted\n");
		print_command(cmd);
                user_disconnect(user);
                return;
	    }
	    break;

	case CONNECTED:

	    /* check for user (client) command */
	    switch (check_cmd(user, cmd, CLIENT_CMD)) {
		case RFAIL:
		    log->debug("debug: fail to run ns client command\n");
		    print_command(cmd);
		    user_disconnect(user);
		    return;
		case RETURN:
		    goto end;
		    break;
	    }
	    break;
    }

    /* send command */
    switch (send_command(server->fd, cmd, &user->email, to)) {

	case AGAIN:
	    server_sched_write(server);
	    return;

	case RFAIL:

	    log->debug("debug: fail to send command to ns server\n");
	    user_disconnect(user);
	    return;

	default: break;
    }

    /* hook for post send commands */
    switch (check_cmd(user, cmd, CLIENT_POST_CMD)) {

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

	case DISCONNECT:
	    user_disconnect(user);
	    /* fallthrough */

	case RETURN:
	    return;
    }

end:
    if (HAS_CMD(client) && client->commands.cmd->cmd.len > 0 &&
	strcasecmp((char *)client->commands.cmd->cmd.s, "ADL") == 0 &&
	client->commands.cmd->payload.len > (CMD_BUF * 5)) {

	command *cmd = client->commands.cmd;
	client->commands.state |= SENDDELAY;
	if (cmd) {
	    has_trid(cmd);
	    client->commands.wait = cmd->trid;
	} else
	    client->commands.state &= ~SENDDELAY;
    }

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

    server_sched_read(server);

    if (!HAS_CMD(client) || (client->commands.state & SENDDELAY))
        return;

    server_sched_write(server);
}

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

    (void) evfd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: ns client write timeout\n");
	user_disconnect(user);
	return;
    }

    /* check for command */
    if (!HAS_CMD(server))
	return;

    /* check state */
    switch (user->state) {

	case PRE_AUTH:
	case USR_TWN_I:
	case USR_TWN_S:

	    /* check for pre-auth server command */
	    if (check_cmd(user, cmd, SERVER_PRE_CMD) == RFAIL) {

                /* pre-auth server command fail or not found */
		log->debug("debug: ns server pre command not accepted\n");
		print_command(cmd);
		user_disconnect(user);
		return;
	    }
	    break;

	case CONNECTED:

	    /* check for server command */
	    switch (check_cmd(user, cmd, SERVER_CMD)) {
		case RFAIL:
		    log->debug("debug: ns server command not accepted\n");
		    print_command(cmd);
		    user_disconnect(user);
		    return;
		case RETURN:
		    goto end;
		    break;
	    }
	    break;
    }

    /* send command */
    switch (send_command(client->fd, cmd, &user->email, to)) {

	case AGAIN:
	    client_sched_write(client);
	    return;

	case RFAIL:

	    log->debug("debug: fail to send ns client command\n");
	    user_disconnect(user);
	    return;

	default: break;
    }

    /* hook for post send commands */
    switch (check_cmd(user, cmd, SERVER_POST_CMD)) {

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

	case DISCONNECT:
	    user_disconnect(user);
	    /* fallthrough */

	case RETURN:
	    return;
    }

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

    client_sched_read(client);

    if (!HAS_CMD(server))
        return;

    client_sched_write(client);
}

void
ns_client_read(const int evfd, short event, void *p) {
 struct user_		*user   = p;
 client_		*client = user->ns.client;
 server_		*server = user->ns.server;
 log_ 			*log    = &config.log;

    (void) evfd;
    if ((event & EV_TIMEOUT)) {

	log->debug("debug: ns client read timeout\n");
	user_disconnect(user);
	return;
    }

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

	case END:
	    log->debug("debug: ns client disconnected\n");
	    user_disconnect(user);
	    return;

	case RFAIL:
	    log->debug("debug fail to read ns client command\n");
	    user_disconnect(user);
	    return;
    }

    client_sched_read(client);
}

server_ *
ns_disconnect(server_ *ns) {
    return(server_disconnect(ns));
}

int
xfr_ns_proxy(struct user_ *user, command *cmd, int args) {
 client_		*proxy;
 string			*port;

    proxy = (client_ *)malloc(sizeof(client_));
    if (proxy == (client_ *)0)
	die_nomem();
    memset(proxy, 0, sizeof(client_));

    /* bind to free port for new client connection */
    proxy->fd = -1;
    port = bind_free_port(config.port_min, config.port_max,
			 &config.defaults.internal_host, &proxy->fd);
    if (port == (string *)0) {
	/* cannot allocate any port */
	free(proxy);
	return(RFAIL);
    }

    /* XFR address rewrite */
    proxy->xfr   = cmd->args[2];
    cmd->args[2] = port;

    /* event set */
    event_set(&proxy->listen, proxy->fd, EV_READ, ns_proxy_client, user);
    event_add(&proxy->listen, &config.timeout_listen);

    /* update user proxy information */
    user->ns.xfr_proxy = proxy;

    return(ROK);
}

int
msnp13_xfr_ns_proxy(struct user_ *user, command *cmd, int args) {
 client_                *proxy;
 string                 *port;

    /*
     * goes like this:
     * XFR <TRiD> NS <ip server:port> U [D]
     */
    if (cmd->args_len < 4 || check_arg(cmd->args[3], "U") == RFAIL)
	return(RFAIL);

    proxy = (client_ *)malloc(sizeof(client_));
    if (proxy == (client_ *)0)
        die_nomem();
    memset(proxy, 0, sizeof(client_));

    /* bind to free port for new client connection */
    proxy->fd = -1;
    port = bind_free_port(config.port_min, config.port_max,
                         &config.defaults.internal_host, &proxy->fd);
    if (port == (string *)0) {
        /* cannot allocate any port */
        free(proxy);
        return(RFAIL);
    }

    /* XFR address rewrite */
    proxy->xfr   = cmd->args[2];
    cmd->args[2] = port;

    /* event set */
    event_set(&proxy->listen, proxy->fd, EV_READ, ns_proxy_client, user);
    event_add(&proxy->listen, &config.timeout_listen);

    /* update user proxy information */
    user->ns.xfr_proxy = proxy;

    return(ROK);
}

void
ns_client(const int listenfd, short event, void *p) {
 struct sockaddr        client_sa;
 struct user_		*user;
 socklen_t              client_sa_len;
 config_		*config = p;
 log_			*log = &config->log;
 int                    client_fd;

    /* 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 ns client connection\n");
	while (close(listenfd) != 0 && errno == EINTR);
	return;
    }

    /* set socket options */
    if (set_options(client_fd) == -1 || set_nonblock(client_fd) == -1) {
        log->debug("debug: unable to set ns client socket options\n");
        while (close(client_fd) != 0 && errno == EINTR);
        return;
    }

    /* max users */
    if (config->max_clients && user_inuse >= config->max_clients) {
        log->debug("debug: Server full - too many clients\n");
        while (close(client_fd) != 0 && errno == EINTR);
        return;
    }

    /* new user */
    user = user_alloc(&client_sa, client_sa_len);
    if (user == NULL) {
        while (close(client_fd) != 0 && errno == EINTR);
        return;
    }

    /* new NS */
    user->ns.server = server_connect(&config->default_ns_host,
				     &config->default_ns_port,
				     ns_server_read, ns_server_write, user);
    if (user->ns.server == (server_ *)0) {
        log->debug("debug: unable to connect to ns server\n");
        while (close(client_fd) != 0 && errno == EINTR);
        return;
    }

    /* new client */
    user->ns.client = client_alloc(client_fd, ns_client_read,
					      ns_client_write, user);
}

void
ns_proxy_client(int listenfd, short event, void *p) {
 struct	sockaddr	client_sa;
 struct user_		*user  = p;
 socklen_t		client_sa_len;
 client_		*proxy = user->ns.xfr_proxy;
 log_			*log   = &config.log;
 int			client_fd;

    if ((event & EV_TIMEOUT)) {
	log->debug("debug: %s: proxy connection timeout\n", &user->email);
	goto error;
    }

    /* 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 ns client connection\n");
	goto error;
    }

    /* set socket options */
    if (set_options(client_fd) == -1 || set_nonblock(client_fd) == -1) {
	log->debug("unable to set socket options on client fd\n");
	while (close(client_fd) != 0 && errno == EINTR);
	return;
    }

    user->ns.client = client_alloc(client_fd, ns_client_read,
					      ns_client_write, user);

error:

    /* free xfr_proxy */
    proxy_free(proxy);
    user->ns.xfr_proxy = (client_ *)0;
    free(proxy);

    return;
}

int
xfr_ns_post_proxy(struct user_ *user, command *cmd, int args) {
 log_ 		*log    = &config.log;
 client_	*client = user->ns.client;
 client_	*proxy  = user->ns.xfr_proxy;
 server_	*server = user->ns.server;

    if (cmd->args_len < 4)
	return(ROK);
    if (check_arg(cmd->args[1], "NS") == RFAIL)
	return(ROK);

    /* close the connection to NS */
    user->ns.server = ns_disconnect(server);

    /* connect to new NS */
    user->ns.server = server_connect2(proxy->xfr, ns_server_read,
						  ns_server_write, user);
    if (user->ns.server == (server_ *)0) {
	log->debug("debug: cannot connect to ns server\n");
	return(RFAIL);
    }

    /* close connection to client */
    user->ns.client = client_disconnect(client);

    return(RETURN);
}

void
proxy_free(client_ *proxy) {
    /* close listenfd and remove events */
    if (EVENT_FD((&proxy->listen)) != -1) {
	event_del(&proxy->listen);
	proxy->listen.ev_fd  = -1;
    }

    /* close proxy fd */
    if (proxy->fd != -1) {
	while (close(proxy->fd) != 0 && errno == EINTR);
	proxy->fd = -1;
    }

    if (proxy->xfr) {
	str_free(proxy->xfr);
	free(proxy->xfr);
    }
}

