/* Copyright (C) 2000-1 drscholl@users.sourceforge.net
   This is free software distributed under the terms of the
   GNU Public License.

   $Id: server_connect.c,v 1.72 2001/02/15 08:39:45 drscholl Exp $ */

/* Modified 30/05/01 : Added include "confdefs.h" since we are not using the
      for Amiga port : CONFIGURE script
*/

#include "confdefs.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include "opennap.h"
#include "debug.h"

static void
try_connect (server_auth_t * auth)
{
    int     f;
    CONNECTION *cli;
    unsigned int ip;

    /* attempt a connection.  we do this nonblocking so that the server
       doesn't halt if it takes a long time to connect */
    f = make_tcp_connection (auth->name, auth->port, &ip);
    if (f == -1)
        return;

    cli = new_connection ();
    if (!cli)
        goto error;
    cli->fd = f;
    cli->host = STRDUP (auth->alias ? auth->alias : auth->name);
    if (!cli->host)
    {
        OUTOFMEMORY ("try_connect");
        goto error;
    }
    cli->server_login = 1;
    if ((cli->opt.auth = CALLOC (1, sizeof (AUTH))) == 0)
    {
        OUTOFMEMORY ("try_connect");
        goto error;
    }
    cli->opt.auth->nonce = generate_nonce ();
    if (!cli->opt.auth->nonce)
    {
        log ("try_connect: could not generate nonce, closing connection");
        goto error;
    }
    cli->ip = BSWAP32 (ip);
    cli->port = auth->port;

    if (add_client (cli, 1/* server connection */))
        goto error;

    return;
  error:
    log ("try_connect: closing connection");
    if (cli)
    {
        CLOSE (cli->fd);
        if (cli->host)
            FREE (cli->host);
        if (cli->opt.auth)
        {
            if (cli->opt.auth->nonce)
                FREE (cli->opt.auth->nonce);
            FREE (cli->opt.auth);
        }
        FREE (cli);
    }
}

void
complete_connect (CONNECTION * con)
{
    /* a previous call to read() may have reset the error code */
    if (con->destroy || check_connect_status (con->fd) != 0)
    {
        notify_mods (SERVERLOG_MODE, "Server link to %s failed", con->host);
        destroy_connection (con);
        return;
    }
    con->connecting = 0;        /* connected now */

    /* clear the write bit and check the read bit */
    clear_write (con->fd);
    set_read (con->fd);

    /* send the login request */
    ASSERT (Server_Name != 0);
    ASSERT (con->server_login == 1);
    ASSERT (con->opt.auth != 0);
    send_cmd (con, MSG_SERVER_LOGIN, "%s %s %d", Server_Name,
              con->opt.auth->nonce, Compression_Level);

    /* we handle the response to the login request in the main event loop so
       that we don't block while waiting for th reply.  if the server does
       not accept our connection it will just drop it and we will detect
       it by the normal means that every other connection is checked */

    log ("complete_connect: connection to %s established", con->host);
}

server_auth_t *
find_server_auth (const char *host)
{
    LIST   *list;
    server_auth_t *auth;

    for (list = Server_Auth; list; list = list->next)
    {
        auth = list->data;
        if (!strcasecmp (host, auth->name) ||
            (auth->alias && !strcasecmp (host, auth->alias)))
            return auth;
    }
    return 0;
}

/* process client request to link another server
 * 10100 [ :<user> ] <server-name> [remote_server]
 */
HANDLER (server_connect)
{
    USER   *user;
    char   *av[3];
    char   *remote_server = Server_Name;
    char   *sender_name;
    int     ac;
    server_auth_t *auth = 0;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    if (pop_user_server (con, tag, &pkt, &sender_name, &user) != 0)
        return;
    ASSERT (validate_user (user));

    if (user->level < LEVEL_ADMIN)
    {
        log ("server_connect: failed request from %s", user->nick);
        send_user (user, MSG_SERVER_NOSUCH,
                   "[%s] server connect failed: permission denied",
                   Server_Name);
        return;                 /* no privilege */
    }

    ac = split_line (av, FIELDS (av), pkt);

    if (ac < 1)
    {
        log ("server_connect: too few parameters");
        send_user (user, MSG_SERVER_NOSUCH,
                   "[%s] server connect failed: missing parameter",
                   Server_Name);
        return;
    }

    /* check to make sure this server is not already linked */
    if (is_linked (av[0]))
    {
        send_user (user, MSG_SERVER_NOSUCH,
                   "[%s] server connect failed: already linked", Server_Name);
        return;
    }

    /* check to see if a remote server was specified.  otherwise we assume
     * link from the local server
     */
    if (ac > 1)
        remote_server = av[1];

    /* determine if the link is supposed to be made from this server */
    if (!strcasecmp (remote_server, Server_Name))
    {
        /* look up the server auth info to find out if we are allowed to
         * link this server
         */
        auth = find_server_auth (av[0]);

        /* if there is no server, or the user attempted to connect the server
         * by its real name and there is a nick defined, report an error.
         */
        if (!auth || (auth->alias && !strcasecmp (av[0], auth->name)))
        {
            send_user (user, MSG_SERVER_NOSUCH,
                       "[%s] server connect failed: no such server",
                       Server_Name);
            return;
        }

        /* check to make sure this server isn't linked.  we use the real name
         * of the server here just because the above check might have been
         * the nick of the server.
         */
        if (is_linked (auth->name))
        {
            send_user (user, MSG_SERVER_NOSUCH,
                       "server connect failed: %s is already linked",
                       auth->alias ? auth->alias : auth->name);
            return;
        }

        /* use the real name when we connect */
        try_connect (auth);

        /* ugh, this has to be here to prevent disclosure of the real dns name
         * of an aliased server.  unfortunately it means that only the server
         * where the link is being made from will see this, but if someone
         * tries to remote connect an aliased server via its real dns name,
         * it would be displayed to all mods+ along the line.
         */
        notify_mods (SERVERLOG_MODE,
                     "%s requested server link from %s to %s:%hu",
                     user->nick, remote_server,
                     /* don't use the real name if the server has a nick */
                     auth->alias ? auth->alias : auth->name, auth->port);
    }
    else if (!is_linked (remote_server))
    {
        send_user (user, MSG_SERVER_NOSUCH,
                   "[%s] server connect failed: no such remote server",
                   Server_Name);
        return;
    }
    else
    {
        ASSERT (remote_server != Server_Name);

        pass_message_args (con, MSG_CLIENT_CONNECT, ":%s %s %s",
                           user->nick, av[0], remote_server);
    }
}

/* 10101 [ :<nick> ] <server> ["reason"]
 * server disconnect/quit notification
 */
HANDLER (server_disconnect)
{
    USER   *user;
    char   *sender_name;
    int     ac = -1;
    char   *av[2];
    LIST   *list;
    CONNECTION *serv;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    if (pop_user_server (con, tag, &pkt, &sender_name, &user) != 0)
        return;
    if (pkt)
        ac = split_line (av, FIELDS (av), pkt);
    if (ac < 1)
    {
        unparsable (con);
        return;
    }

    if (user)
    {
        ASSERT (validate_user (user));
        if (user->level < LEVEL_ADMIN)
        {
            if (ISUSER (con))
                send_cmd (con, MSG_SERVER_NOSUCH,
                          "server disconnect failed: permission denied");
            return;
        }
    }

    if (!is_linked (av[0]))
    {
        if (user && ISUSER (user->con))
            send_user (user, MSG_SERVER_NOSUCH,
                    "server disconnect failed: no such server");
        else
            log("server_disconnect: %s is not linked", av[0]);
        return;
    }

    /* if the server is locally connected, mark it for disconnection */
    for (list = Servers; list; list = list->next)
    {
        serv = list->data;
        if (!strcasecmp (av[0], serv->host))
        {
            serv->quit = 1;     /* note that we received a quit message */
            destroy_connection (serv);
            break;
        }
    }

    pass_message_args (con, MSG_CLIENT_DISCONNECT, ":%s %s \"%s\"",
                       sender_name, av[0], (ac > 1) ? av[1] : "");

    /* remove all links behind this server */
    remove_links (av[0]);

    notify_mods (SERVERLOG_MODE, "Server %s has quit: %s (%s)",
            av[0], (ac > 1) ? av[1] : "", sender_name);
}

/* 10110 [ :<user> ] <server> [ "<reason>" ] */
/* force the server process to die */
HANDLER (kill_server)
{
    USER   *user;
    int     ac = -1;
    char   *av[2];

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    ASSERT (pkt != 0);
    if (pop_user (con, &pkt, &user) != 0)
        return;
    if (pkt)
        ac = split_line (av, FIELDS (av), pkt);
    if (ac < 1)
    {
        unparsable (con);
        return;
    }
    ASSERT (validate_user (user));
    if (user->level < LEVEL_ELITE)
    {
        permission_denied (con);
        return;
    }

    if (!is_linked (av[0]) && strcasecmp (Server_Name, av[0]) != 0)
    {
        send_user (user, MSG_SERVER_NOSUCH,
                   "kill server failed: no such server");
        return;
    }

    if (ac > 1)
        truncate_reason (av[1]);

    pass_message_args (con, MSG_CLIENT_KILL_SERVER, ":%s %s \"%s\"",
                       user->nick, av[0], (ac > 1) ? av[1] : "");

    notify_mods (SERVERLOG_MODE, "%s killed server %s: %s", user->nick,
                 av[0], (ac > 1) ? av[1] : "");

    if (!strcasecmp (av[0], Server_Name))
    {
        log ("kill_server(): shutdown by %s: %s", user->nick,
             (ac > 1) ? av[1] : "");
        SigCaught = 1;          /* this causes the main event loop to exit */
    }
}

/* 10111 <server> [ <reason> ] */
HANDLER (remove_server)
{
    char   *reason;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    /* TODO: should we be able to remove any server, or just from the local
       server? */
    CHECK_USER_CLASS ("remove_server");
    ASSERT (validate_user (con->user));
    if (con->user->level < LEVEL_ELITE)
    {
        permission_denied (con);
        return;
    }
    reason = strchr (pkt, ' ');
    if (reason)
        *reason++ = 0;
    snprintf (Buf, sizeof (Buf), "DELETE FROM servers WHERE server = '%s'",
              pkt);
}

/* 801 [ :<user> ] [ <server> ] */
HANDLER (server_version)
{
    USER   *user;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    if (pop_user (con, &pkt, &user) != 0)
        return;
    ASSERT (validate_user (user));
    if (user->level < LEVEL_MODERATOR)
    {
        if (con->class == CLASS_USER)
            permission_denied (con);
        return;
    }
    if (!*pkt || !strcmp (Server_Name, pkt))
    {
        send_user (user, MSG_SERVER_NOSUCH, "--");
        send_user (user, MSG_SERVER_NOSUCH, "%s %s", PACKAGE, VERSION);
        send_user (user, MSG_SERVER_NOSUCH, "--");
    }
    else
        pass_message_args (con, tag, ":%s %s", user->nick, pkt);
}

/* 0/404 <message> */
HANDLER (server_error)
{
    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    CHECK_SERVER_CLASS ("server_error");
    notify_mods (ERROR_MODE, "Server %s sent error: %s", con->host, pkt);
}

int
is_linked (const char *host)
{
    LIST   *list;
    LINK   *link;
    CONNECTION *serv;

    if (!strcasecmp (host, Server_Name))
        return 1;               /* self */

    /* check local links */
    for (list = Servers; list; list = list->next)
    {
        serv = list->data;
        if (!strcasecmp (serv->host, host))
            return 1;
    }

    /* check remote links */
    for (list = Server_Links; list; list = list->next)
    {
        link = list->data;
        if (!strcasecmp (link->server, host)
            || !strcasecmp (link->peer, host))
            return 1;
    }
    return 0;
}

/* auto link our servers */
void
auto_link (void)
{
    LIST   *list;

    for (list = Server_Auth; list; list = list->next)
        try_connect (list->data);
}
