/************************************************************************
 *   IRC - Internet Relay Chat, server/s_exit.c
 *
 *   Copyright (C) 2000-2003 TR-IRCD Development
 *
 *   Copyright (C) 1990 Jarkko Oikarinen and
 *                      University of Oulu, Co Center
 *
 *   See file AUTHORS in IRC package for additional names of
 *   the programmers.
 *
 *   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, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * $Id: s_exit.c,v 1.11 2004/02/24 19:03:33 tr-ircd Exp $ 
 */

#include "struct.h"
#include "common.h"
#include "sys.h"
#include "numeric.h"
#include "zlink.h"
#include "dh.h"
#include "h.h"
#include "msg.h"
#include "listener.h"
#include "resnew.h"
#include "s_auth.h"
#include "s_conf.h"
#include "event.h"

static void exit_one_client(aClient *, aClient *, char *, int);

/*
 * NOQUIT
 * a method of reducing the stress on the network during server splits
 * by sending only a simple "SQUIT" message for the server that is dropping,
 * instead of thousands upon thousands of QUIT messages for each user,
 * plus an SQUIT for each server behind the dead link.
 *
 * Original idea by Cabal95, implementation by lucas
 */

/*
 * exit_recursion
 * 
 * recursive function!
 * therefore, we pass dead and reason to ourselves.
 * in the beginning, dead == cptr, so it will be the one
 * out of the loop last. therefore, dead should remain a good pointer.
 * dead: the actual server that split (if this belongs to us, we
 *       absolutely CANNOT send to it)
 * source_p: the client that caused this split
 * spinfo: split reason, as generated in exit_server
 * comment: comment provided
 */

static inline void exit_recursion(aClient *dead, aClient *source_p, char *spinfo)
{
    aClient *acptr;

    if (IsMe(dead))
	return;
    if (dead->serv == NULL)
	return;

    /*
     * okay, this is annoying.
     * first off, we need two loops.
     * one: to remove all the clients.
     * two: to remove all the servers.
     * HOWEVER! removing a server may cause removal of more servers and more clients.
     * and this may make our pointer to next bad. therefore, we have to restart
     * the server loop each time we find a server.
     * We _NEED_ two different loops: all clients must be removed before the server is
     * removed. Otherwise, bad things (tm) can happen.
     */

    logevent_call(LogSys.server_noquit, "BEGIN NOQUIT RECURSION", dead);

    while ((acptr = dead->serv->users)) {
        exit_one_client(acptr, dead, spinfo, 1);
        SetExited(acptr);
	remove_client_from_list(acptr);
    }
    while ((acptr = dead->serv->servers)) {
        exit_recursion(acptr, source_p, spinfo);
        exit_one_client(acptr, source_p, spinfo, 1);
        SetExited(acptr);
	remove_client_from_list(acptr);
    }

    logevent_call(LogSys.server_noquit, "END NOQUIT RECURSION", dead);
}

/*
 * * exit_client *    
 * 
 * This function exits a client of *any* type (user, server, etc)
 * from this server. Also, this generates all necessary prototol
 * messages that this exit may cause. 
 * 
 *   1) If the client is a local client, then this implicitly exits
 * all other clients depending on this connection (e.g. remote
 * clients having 'from'-field that points to this. 
 * 
 *   2) If the client is a remote client, then only this is exited.
 * 
 * For convenience, this function returns a suitable value for
 * m_function return value: 
 * 
 */

int exit_client(aClient *exit_ptr,	/* Client exiting */
		aClient *source_p,	/* Client generating this Exit, !NULL */
		char *comment	/* Reason for the exit */
    )
{
    dlink_node *m;
    aConfItem *aconf;
    // logevent_call(LogSys.exit_client, exit_ptr, source_p, comment);

    if (IsMe(exit_ptr))
	return 0;

    if (IsExited(exit_ptr) || IsClosing(exit_ptr) || IsHttpClient(exit_ptr))
	return CLIENT_EXITED;

    SetClosing(exit_ptr);

    if (exit_ptr->fd >= 0) {
	if (!IsDead(exit_ptr))
	    send_queued(exit_ptr->fd, exit_ptr);
	sendto_one_server(exit_ptr, NULL, TOK1_ERROR,
			  ":Closing Link: %s (%s)",
			  IsPerson(exit_ptr) ? exit_ptr->sockhost : "0.0.0.0", comment);
    }

    if (IsLocalClient(exit_ptr)) {
    	if (IsListenerHttp(exit_ptr->listener))
	    return CLIENT_EXITED;
	delete_adns_queries(exit_ptr->dns_query);
	delete_identd_queries(exit_ptr);
        m = dlinkFind(&unknown_list, exit_ptr);
        if (m != NULL) {
            Count.unknown--;
            dlinkDelete(m, &unknown_list);
            free_dlink_node(m);
        }

	if (IsClient(exit_ptr)) {
	    Count.local--;
	    if (IsAnOper(exit_ptr)) {
		m = dlinkFind(&locoper_list, exit_ptr);
		if (m != NULL) {
		    dlinkDelete(m, &locoper_list);
		    free_dlink_node(m);
		}
	    }
	    if (IsPerson(exit_ptr)) {	/* a little extra paranoia */
		m = dlinkFind(&lclient_list, exit_ptr);
		if (m != NULL) {
		    dlinkDelete(m, &lclient_list);
		    free_dlink_node(m);
		}
	    }
	    ircstp->is_cl++;
	    ircstp->is_cbs += exit_ptr->sendB;
	    ircstp->is_cbr += exit_ptr->receiveB;
	    ircstp->is_cks += exit_ptr->sendK;
	    ircstp->is_ckr += exit_ptr->receiveK;
	    ircstp->is_cti += timeofday - exit_ptr->firsttime;
	    if (ircstp->is_cbs > 2047) {
		ircstp->is_cks += (ircstp->is_cbs >> 10);
		ircstp->is_cbs &= 0x3ff;
	    }
	    if (ircstp->is_cbr > 2047) {
		ircstp->is_ckr += (ircstp->is_cbr >> 10);
		ircstp->is_cbr &= 0x3ff;
	    }
	}

	if (IsNegoServer(exit_ptr))
	    sendto_lev(SNOTICE_LEV, "Lost server %s during negotiation: %s",
		       exit_ptr->name, comment);
	if (IsServer(exit_ptr)) {
	    Count.myserver--;
	    if (IsULine(exit_ptr))
		Count.myulined--;
	    m = dlinkFind(&serv_list, exit_ptr);
	    if (m != NULL) {
		dlinkDelete(m, &serv_list);
		free_dlink_node(m);
	    }
	    sendto_gnotice("%C was connected for %lu seconds.  %lu/%lu sendK/recvK.",
			   exit_ptr, timeofday - exit_ptr->firsttime, exit_ptr->sendK,
			   exit_ptr->receiveK);
	    ircstp->is_sv++;
	    ircstp->is_sbs += exit_ptr->sendB;
	    ircstp->is_sbr += exit_ptr->receiveB;
	    ircstp->is_sks += exit_ptr->sendK;
	    ircstp->is_skr += exit_ptr->receiveK;
	    ircstp->is_sti += timeofday - exit_ptr->firsttime;
	    if (ircstp->is_sbs > 2047) {
		ircstp->is_sks += (ircstp->is_sbs >> 10);
		ircstp->is_sbs &= 0x3ff;
	    }
	    if (ircstp->is_sbr > 2047) {
		ircstp->is_skr += (ircstp->is_sbr >> 10);
		ircstp->is_sbr &= 0x3ff;
	    }
	    /*
	     * If the connection has been up for a long amount of time, schedule
	     * a 'quick' reconnect, else reset the next-connect cycle.
	     */
	    if ((aconf = find_conf_exact(exit_ptr->name, exit_ptr->username,
					 exit_ptr->sockhost, CONF_SERVER))) {
		/*
		 * Reschedule a faster reconnect, if this was a automatically
		 * connected configuration entry. (Note that if we have had
		 * a rehash in between, the status has been changed to
		 * CONF_ILLEGAL). But only do this if it was a "good" link.
		 */
		aconf->hold = time(NULL);
		aconf->hold += (aconf->hold - exit_ptr->since > HANGONGOODLINK) ?
		    HANGONRETRYDELAY : aconf->class->connect_frequency;
	    }
            if (GeneralOpts.split == 0)
                eventAddIsh("check_splitmode", check_splitmode, NULL, 1800);
            GeneralOpts.split = 1; 
	
	}
	if (!IsClient(exit_ptr) && !IsServer(exit_ptr))
	    ircstp->is_ni++;

	if (IsPerson(exit_ptr)) {
	    hash_del_watch_list(exit_ptr);
	    sendto_lev(CCONN_LEV,
		       "Client exiting: %^C [%s] [%s]",
		       exit_ptr,
		       (exit_ptr->flags & FLAGS_NORMALEX) ? "Client Quit" : comment,
		       exit_ptr->hostip);
	}

	if (IsService(exit_ptr)) {
	    sendto_lev(SERVICE_LEV, "Exiting Service %s (%s@%s)",
		       exit_ptr->name, exit_ptr->username, exit_ptr->sockhost);
	    Count.myservice--;
	}
	det_confs_butmask(exit_ptr, 0);
	exit_one_client(exit_ptr, source_p, comment, 0);
	if (exit_ptr->fd >= 0) {
#ifdef HAVE_ENCRYPTION_ON
	    /* Close the ssl connection also */
	    if (IsSSL(exit_ptr)) {
		SSL_set_shutdown(exit_ptr->ssl, SSL_RECEIVED_SHUTDOWN);
		SSL_smart_shutdown(exit_ptr->ssl);
		SSL_free(exit_ptr->ssl);
		exit_ptr->ssl = NULL;
	    }
#endif
	    fd_close(exit_ptr->fd);
	    exit_ptr->fd = -1;
	    exit_ptr->flags |= FLAGS_DEADSOCKET;
	}
	linebuf_donebuf(&exit_ptr->sendQ);
	linebuf_donebuf(&exit_ptr->recvQ);
	memset(exit_ptr->passwd, 0, sizeof(exit_ptr->passwd));
	/* clean up extra sockets from P-lines which have been discarded. */
	if (exit_ptr->listener) {
	    assert(0 < exit_ptr->listener->ref_count);
	    if (0 == --exit_ptr->listener->ref_count && !exit_ptr->listener->active)
		free_listener(exit_ptr->listener);
	    exit_ptr->listener = 0;
	}
	m = make_dlink_node();
	dlinkAdd(exit_ptr, m, &exit_list);
    } else if (!IsLocalClient(exit_ptr)) {
	// exit_ptr->from = NULL;	/* ...this should catch them! >:) --msa */
	exit_one_client(exit_ptr, source_p, comment, 0);
	remove_client_from_list(exit_ptr);
    }
    SetExited(exit_ptr);
    return CLIENT_EXITED;
}

/*
 * * Exit one client, local or remote. Assuming all dependants have *
 * been already removed, and socket closed for local client.
 */
static void exit_one_client(aClient *exit_ptr, aClient *source_p, char *comment, int split)
{
    dlink_node *lp, *ptr;
    dlink_node *next_lp;
    aClient *acptr;
    if (IsMe(exit_ptr))
	return;
    if (IsClient(exit_ptr)) 
        Count.total--;
    if (IsAnOper(exit_ptr))
        Count.oper--;
    if (IsInvisible(exit_ptr))
        Count.invisi--;
    if (IsServer(exit_ptr)) {

    char splitname[HOSTLEN + HOSTLEN + 2];
	if (ServerHide.enable) {
	    if (ServerHide.flatten_links)
		ircsprintf(splitname, "%C %*C", &me, exit_ptr);
	    else
		ircsprintf(splitname, "a.server.on.%s %C", ServerInfo.networkname, &me);
	} else {
	    ircsprintf(splitname, "%*C %*C", exit_ptr->servptr, exit_ptr);
	}
	logevent_call(LogSys.exit_server, exit_ptr, source_p, comment);
	exit_recursion(exit_ptr, source_p, splitname);
	for (ptr = serv_list.head; ptr; ptr = next_lp) {
	    next_lp = ptr->next;
	    acptr = ptr->data;
	    if (!acptr || IsMe(acptr) || acptr == exit_ptr || acptr == source_p)
		continue;
	    if (exit_ptr->from == acptr)	/* "upstream" squit */
		sendto_one_server(acptr, source_p, TOK1_SQUIT, "%~C :%s", exit_ptr, comment);
	    else
		sendto_one_server(acptr, NULL, TOK1_SQUIT, "%~C :%s", exit_ptr, comment);
	}

	if (HasID(exit_ptr))
	    rem_base64_server(exit_ptr);
	remove_server_from_list(exit_ptr);
	if (exit_ptr->servptr && exit_ptr->servptr->serv) {
	    del_client_from_llist(&(exit_ptr->servptr->serv->servers), exit_ptr);
	    exit_ptr->servptr->serv->servercnt--;
	}
	Count.server--;
	if (exit_ptr->serv) {
#ifdef HAVE_ENCRYPTION_ON
	    if (exit_ptr->serv->sin)
		dh_end_session(exit_ptr->serv->sin);
	    if (exit_ptr->serv->sout)
		dh_end_session(exit_ptr->serv->sout);
	    if (exit_ptr->serv->rc4_in)
		rc4_destroystate(exit_ptr->serv->rc4_in);
	    if (exit_ptr->serv->rc4_out)
		rc4_destroystate(exit_ptr->serv->rc4_out);
#endif
	    if (exit_ptr->serv->zin)
		destroy_unzipstream(exit_ptr->serv->zin);
	    if (exit_ptr->serv->zout)
		destroy_zipstream(exit_ptr->serv->zout);
	    exit_ptr->serv->nline = NULL;
	    MyFree((char *) exit_ptr->serv);
	}
    }

    if (IsPerson(exit_ptr)) {
	/*
	 * * If a person is on a channel, send a QUIT notice * to every
	 * client (person) on the same channel (so * that the client can
	 * show the "**signoff" message). * (Note: The notice is to the
	 * local clients *only*)
	 */
	/*
	 * If this exit is generated from "m_kill", then there is no
	 * sense in sending the QUIT--KILL's have been sent instead.
	 */
	if (!split) {
	    if ((exit_ptr->flags & FLAGS_KILLED) == 0) {
		sendto_serv_butone(exit_ptr->from, exit_ptr, TOK1_QUIT, ":%s", comment);
	    }
	}
	send_quit_to_common_channels(exit_ptr, comment);
	if (MyClient(exit_ptr))
	    free_localuser_identity(exit_ptr);
	sendto_service(SERVICE_SEE_QUITS, 0, exit_ptr, NULL, TOK1_QUIT, ":%s", comment);
	for (lp = exit_ptr->user->channel.head; lp; lp = next_lp) {
	    next_lp = lp->next;
	    remove_user_from_channel(exit_ptr, lp->data);
	}
	for (lp = exit_ptr->user->invited.head; lp; lp = next_lp) {
	    next_lp = lp->next;
	    del_invite(exit_ptr, lp->data);
	}
	for (lp = exit_ptr->user->silence.head; lp; lp = next_lp) {
	    next_lp = lp->next;
	    del_silence(exit_ptr, lp->data);
	}
	remove_dcc_references(exit_ptr);
	remove_accept_references(exit_ptr);
	if (HasID(exit_ptr))
	    rem_userid_from_server(exit_ptr->servptr, exit_ptr);
	if (IsRegisteredUser(exit_ptr))
	    hash_check_watch(exit_ptr, RPL_LOGOFF);
	if (exit_ptr->servptr && exit_ptr->servptr->serv) {
	    del_client_from_llist(&(exit_ptr->servptr->serv->users), exit_ptr);
	    exit_ptr->servptr->serv->usercnt--;
	}
	add_history(exit_ptr, 0);
	off_history(exit_ptr);
	free_user(exit_ptr->user);
	exit_ptr->user = NULL;
    }
    if (IsService(exit_ptr)) {
	if (exit_ptr->service) {
	    sendto_lev(SERVICE_LEV,
		       "Received QUIT from service %s (%s@%s)",
		       exit_ptr->name, exit_ptr->username, exit_ptr->sockhost);
	    if (exit_ptr->from == exit_ptr)
		sendto_lev(SERVICE_LEV, "Service %s disconnected", exit_ptr->name);
	}
	Count.service--;
	if (exit_ptr->service)
	    free_service(exit_ptr);
    }

    del_from_client_hash_table(exit_ptr->name, exit_ptr);

    return;
}

void remove_exited_clients(void *notused)
{
    dlink_node *ptr, *prev_ptr;
    aClient *cptr;
    for (ptr = exit_list.tail; ptr; ptr = prev_ptr) {
	prev_ptr = ptr->prev;
	cptr = ptr->data;
	if (!cptr)
	    continue;
	if (IsExited(cptr)) {
	    remove_client_from_list(cptr);
	    dlinkDeleteNode(ptr, &exit_list);
	}
    }
}


syntax highlighted by Code2HTML, v. 0.9.1