/* dircproxy
 * Copyright (C) 2002 Scott James Remnant <scott@netsplit.com>.
 * All Rights Reserved.
 *
 * irc_client.c
 *  - Handling of clients connected to the proxy
 *  - Functions to send data to the client in the correct protocol format
 * --
 * @(#) $Id: irc_client.c,v 1.83 2002/02/06 10:10:00 scott Exp $
 *
 * This file is distributed according to the GNU General Public
 * License.  For full details, read the top of 'main.c' or the
 * file called COPYING that was distributed with this code.
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>

#include <dircproxy.h>

#ifdef HAVE_CRYPT_H
# include <crypt.h>
#else /* HAVE_CRYPT_H */
# include <unistd.h>
#endif /* HAVE_CRYPT_H */

#include "sprintf.h"
#include "net.h"
#include "dns.h"
#include "timers.h"
#include "dcc_net.h"
#include "irc_log.h"
#include "irc_net.h"
#include "irc_prot.h"
#include "irc_string.h"
#include "irc_server.h"
#include "irc_client.h"
#include "logo.h"
#include "help.h"

/* forward declarations */
static void _ircclient_connected2(struct ircproxy *, void *, struct in_addr *,
                                  const char *);
static void _ircclient_data(struct ircproxy *, int);
static void _ircclient_error(struct ircproxy *, int, int);
static int _ircclient_detach(struct ircproxy *, const char *);
static int _ircclient_gotmsg(struct ircproxy *, const char *);
static int _ircclient_authenticate(struct ircproxy *, const char *);
static void _ircclient_resetnick(struct ircproxy *, void *);
static int _ircclient_got_details(struct ircproxy *, const char *,
                                  const char *, const char *, const char *);
static int _ircclient_motd(struct ircproxy *);
static void _ircclient_timedout(struct ircproxy *, void *);
static int _ircclient_send_dccreject(struct ircproxy *, const char *);

/* New user mode bits */
#define RFC2812_MODE_W 0x04
#define RFC2812_MODE_I 0x08

/* Time/date format for strftime(3) */
#define START_TIMEDATE_FORMAT "%a, %d %b %Y %H:%M:%S %z"

/* Define MIN() */
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif /* MIN */

/* Called when a new client has connected */
int ircclient_connected(struct ircproxy *p) {
  ircclient_send_notice(p, "Looking up your hostname...");

  dns_hostfromaddr((void *)p, 0, p->client_addr.sin_addr,
                   DNS_FUNCTION(_ircclient_connected2));

  return 0;
}

/* Called once a client DNS lookup has completed */
static void _ircclient_connected2(struct ircproxy *p, void *data,
                                  struct in_addr *addr, const char *name) {
  p->client_host = x_strdup(name);
  if (!p->hostname)
    p->hostname = x_strdup(name);
  ircclient_send_notice(p, "Got your hostname.");

  p->client_status |= IRC_CLIENT_CONNECTED;
  net_hook(p->client_sock, SOCK_NORMAL, (void *)p,
           ACTIVITY_FUNCTION(_ircclient_data),
           ERROR_FUNCTION(_ircclient_error));

  debug("Client connected from %s", p->client_host);

  timer_new((void *)p, "client_auth", g.client_timeout,
            TIMER_FUNCTION(_ircclient_timedout), (void *)0);
}

/* Called when a client sends us stuff. */
static void _ircclient_data(struct ircproxy *p, int sock) {
  char *str;

  if (sock != p->client_sock) {
    error("Unexpected socket %d in _ircclient_data, expected %d", sock,
          p->client_sock);
    net_close(&sock);
    return;
  }

  str = 0;
  while (!p->dead && (p->client_status & IRC_CLIENT_CONNECTED)
         && net_gets(p->client_sock, &str, "\r\n") > 0) {
    debug(">> '%s'", str);
    _ircclient_gotmsg(p, str);
    free(str);
  }
}

/* Called on client disconnection or error */
static void _ircclient_error(struct ircproxy *p, int sock, int bad) {
  if (sock != p->client_sock) {
    error("Unexpected socket %d in _ircclient_error, expected %d", sock,
          p->client_sock);
    net_close(&sock);
    return;
  }

  if (bad) {
    debug("Socket error");
  } else {
    debug("Client disconnect");
  }

  _ircclient_detach(p, 0);
}

/* Called to detach an irc client */
static int _ircclient_detach(struct ircproxy *p, const char *message) {
  if (p->die_on_close) {
    debug("Killing proxy");

    if (message) {
      ircserver_send_command(p, "QUIT", ":%s", message);
    } else if (p->conn_class && p->conn_class->quit_message) {
      ircserver_send_command(p, "QUIT", ":%s", p->conn_class->quit_message);
    } else {
      ircserver_send_command(p, "QUIT", ":Leaving IRC - %s %s",
                             PACKAGE, VERSION);
    }
    ircserver_close_sock(p);

    p->conn_class = 0;
    ircclient_close(p);

  } else {
    debug("Detaching proxy");
    if ((p->client_status == IRC_CLIENT_ACTIVE)
        && (p->conn_class->log_events & IRC_LOG_CLIENT))
      irclog_notice(p, 0, PACKAGE, "You disconnected");

    /* Drop modes */
    if ((p->client_status == IRC_CLIENT_ACTIVE)
        && p->conn_class->drop_modes) {
      char *mode;

      mode = x_sprintf("-%s", p->conn_class->drop_modes);
      debug("Auto-mode-change '%s'", mode);

      ircclient_change_mode(p, mode);
      if (p->server_status == IRC_SERVER_ACTIVE)
        ircserver_send_command(p, "MODE", "%s %s", p->nickname, mode);

      free(mode);
    }

    /* Send detach message to all channels we're on */
    if ((p->server_status == IRC_SERVER_ACTIVE)
        && (p->client_status == IRC_CLIENT_ACTIVE)) {
      if (p->conn_class->detach_message) {
        struct ircchannel *c;
        int slashme;
        char *msg;

        msg = p->conn_class->detach_message;
        if ((strlen(msg) >= 5) && !strncasecmp(msg, "/me ", 4)) {
          /* Starts with /me */
          slashme = 1;
          msg += 4;
        } else {
          slashme = 0;
        }

        c = p->channels;
        while (c) {
          if (!c->inactive && !c->unjoined) {
            if (slashme) {
              ircserver_send_command(p, "PRIVMSG", "%s :\001ACTION %s\001",
                                     c->name, msg);
            } else {
              ircserver_send_command(p, "PRIVMSG", "%s :%s", c->name, msg);
            }
          }
          c = c->next;
        }
      }
    }

    /* Leave channels until they come back */
    if ((p->server_status == IRC_SERVER_ACTIVE)
        && (p->client_status == IRC_CLIENT_ACTIVE)) {
      if (p->conn_class->channel_leave_on_detach) {
        struct ircchannel *c;

        c = p->channels;
        while (c) {
          struct ircchannel *t;

          t = c;
          c = c->next;

          /* Leave the channel and decide whether to delete it or rejoin */
          if (!t->inactive && !t->unjoined) {
            ircserver_send_command(p, "PART", ":%s", t->name);
            if (p->conn_class->channel_rejoin_on_attach) {
              t->unjoined = 1;
            } else {
              ircnet_delchannel(p, t->name);
            }
          }
        }
      }
    }
 
    /* Set away message */
    if ((p->server_status == IRC_SERVER_ACTIVE)
        && (p->client_status == IRC_CLIENT_ACTIVE)) {
      if (message) {
        ircserver_send_command(p, "AWAY", ":%s", message);
      } else if (!p->awaymessage && p->conn_class->away_message) {
        ircserver_send_command(p, "AWAY", ":%s", p->conn_class->away_message);
      }
    }

    /* Change Nickname */
    if ((p->client_status == IRC_CLIENT_ACTIVE)
        && p->conn_class->detach_nickname) {
      char *nick, *ptr;

      nick = x_strdup(p->conn_class->detach_nickname);
      ptr = strchr(nick, '*');
      if (ptr) {
        char *newnick;

        *(ptr++) = 0;
        newnick = x_sprintf("%s%s%s", nick, p->nickname, ptr);
        free(nick);
        nick = newnick;
      }
      debug("Auto-nick-change '%s'", nick);

      /* We need to remember what the setnickname is now so when the client
         comes back we can reset it again.  So put it in oldnickname. */
      if (p->oldnickname)
        free(p->oldnickname);
      p->oldnickname = p->setnickname;
      p->setnickname = 0;

      ircclient_change_nick(p, nick);

      free(nick);
    }
     
    /* Open other_log */
    if ((p->client_status == IRC_CLIENT_ACTIVE)
        && p->conn_class->other_log_enabled
        && !p->conn_class->other_log_always) {
      if (irclog_open(p, p->nickname))
        ircclient_send_notice(p, "(warning) Unable to log server/private "
                              "messages");
    }

    /* Open channel logs */
    if ((p->client_status == IRC_CLIENT_ACTIVE)
        && p->conn_class->chan_log_enabled
        && !p->conn_class->chan_log_always) {
      struct ircchannel *c;

      c = p->channels;
      while (c) {
        if (irclog_open(p, c->name))
          ircclient_send_notice(p, "(warning) Unable to log channel: %s",
                                c->name);
        c = c->next;
      }
    }

    /* Close the socket */
    ircclient_close(p);
  }

  return 0;
}

/* Called when we get an irc protocol data from a client */
static int _ircclient_gotmsg(struct ircproxy *p, const char *str) {
  struct ircmessage msg;

  if (ircprot_parsemsg(str, &msg) == -1)
    return -1;

  debug("c=%02x, s=%02x", p->client_status, p->server_status);

  if (!(p->client_status & IRC_CLIENT_AUTHED)) {
    /* Accept PASS, NICK and USER commands only until we've authenticated */
    if (!irc_strcasecmp(msg.cmd, "PASS")) {
      if (msg.numparams >= 1) {
        if (p->password)
          free(p->password);
        p->password = x_strdup(msg.params[0]);
        p->client_status |= IRC_CLIENT_GOTPASS;
      } else {
        ircclient_send_numeric(p, 461, ":Not enough parameters");
      }

    } else if (!irc_strcasecmp(msg.cmd, "NICK")) {
      if (msg.numparams >= 1) {
        if (!(p->client_status & IRC_CLIENT_GOTNICK)
            || strcmp(p->nickname, msg.params[0]))
          ircclient_change_nick(p, msg.params[0]);
      } else {
        ircclient_send_numeric(p, 431, ":No nickname given");
      }

    } else if (!irc_strcasecmp(msg.cmd, "USER")) {
      if (msg.numparams >= 4) {
        if (!(p->client_status & IRC_CLIENT_GOTUSER))
          _ircclient_got_details(p, msg.params[0], msg.params[1],
                                 msg.params[2], msg.params[3]);
      } else {
        ircclient_send_numeric(p, 461, ":Not enough parameters");
      }
    
    } else if (!(p->client_status & IRC_CLIENT_GOTPASS)) {
      ircclient_send_notice(p, "Please send /QUOTE PASS <password> to login");

    } else {
      ircclient_send_notice(p, "Please send /QUOTE NICK and /QUOTE USER");
    }

  } else if (!(p->client_status & IRC_CLIENT_GOTNICK)) {
    /* We've lost the nickname */
    if (!irc_strcasecmp(msg.cmd, "NICK")) {
      if (msg.numparams >= 1) {
        ircclient_change_nick(p, msg.params[0]);
      } else {
        ircclient_send_numeric(p, 431, ":No nickname given");
      }

    } else {
      ircclient_send_notice(p, "Please send a /NICK command");
    }

  } else {
    /* The server MUST be active to use most of the commands.  The only
       exception is /DIRCPROXY. */
    if (p->server_status == IRC_SERVER_ACTIVE) {
      /* By default we squelch everything, but the else clause turns this off.
         Effectively it means that all handled commands are not passed to the
         server unless you set squelch to 0 */
      int squelch = 1;

      if (!irc_strcasecmp(msg.cmd, "PASS")) {
        /* Ignore PASS */
      } else if (!irc_strcasecmp(msg.cmd, "USER")) {
        /* Ignore USER */
      } else if (!irc_strcasecmp(msg.cmd, "DIRCPROXY")) {
        /* Ignore DIRCPROXY (handled in a minute) */
      } else if (!irc_strcasecmp(msg.cmd, "QUIT")) {
        /* User wants to detach */
        ircnet_announce_status(p);
        ircclient_send_error(p, "Detached from %s %s", PACKAGE, VERSION);
        _ircclient_detach(p, 0);
        ircprot_freemsg(&msg);
        return 0;

      } else if (!irc_strcasecmp(msg.cmd, "PONG")) {
        /* Ignore PONG */
      } else if (!irc_strcasecmp(msg.cmd, "NICK")) {
        /* User changing their nickname */
        if (msg.numparams >= 1) {
          ircclient_change_nick(p, msg.params[0]);
        } else {
          ircclient_send_numeric(p, 431, ":No nickname given");
        }

      } else if (!irc_strcasecmp(msg.cmd, "AWAY")) {
        /* User marking themselves as away or back */
        squelch = 0;

        /* ircII sends an empty parameter to mark back *grr* */
        if ((msg.numparams >= 1) && strlen(msg.params[0])) {
          free(p->awaymessage);
          p->awaymessage = x_strdup(msg.params[0]);
        } else {
          free(p->awaymessage);
          p->awaymessage = 0;
        }
        
      } else if (!irc_strcasecmp(msg.cmd, "MOTD")) {
        /* User requesting the message of the day from the server */
        p->allow_motd = 1;
        squelch = 0;

      } else if (!irc_strcasecmp(msg.cmd, "PING")) {
        /* User requesting a ping from the server */
        p->allow_pong = 1;
        squelch = 0;

      } else if (!irc_strcasecmp(msg.cmd, "PRIVMSG")) {
        /* All PRIVMSGs go to the server unless we fiddle */
        squelch = 0;

        if (msg.numparams >= 2) {
          struct strlist *list, *s;
          char *str;

          ircprot_stripctcp(msg.params[1], &str, &list);

          /* Privmsgs from us get logged */
          if (str && strlen(str)) {
            if (p->conn_class->log_events & IRC_LOG_TEXT) {
              struct ircchannel *c;

              c = ircnet_fetchchannel(p, msg.params[0]);
              if (c) {
                char *tmp;

                tmp = x_sprintf("%s!%s@%s", p->nickname, p->username,
                                p->hostname);
                irclog_msg(p, msg.params[0], tmp, "%s", str);
                free(tmp);
              }
            }
          }
          free(str);

          /* Handle CTCP */
          str = x_strdup(msg.params[1]);
          s = list;
          while (s) {
            struct ctcpmessage cmsg;
            struct strlist *n;
            char *unquoted;
            int r;

            n = s->next;
            r = ircprot_parsectcp(s->str, &cmsg);
            unquoted = s->str;
            free(s);
            s = n;
            if (r == -1) {
              free(unquoted);
              continue;
            }

            if (!strcmp(cmsg.cmd, "ACTION")) {
              if (p->conn_class->log_events & IRC_LOG_ACTION) {
                struct ircchannel *c;

                c = ircnet_fetchchannel(p, msg.params[0]);
                if (c) {
                  char *tmp;

                  tmp = x_sprintf("%s!%s@%s", p->nickname, p->username,
                                  p->hostname);
                  irclog_ctcp(p, msg.params[0], tmp, "%s", cmsg.orig);
                  free(tmp);
                }
              }
              
            } else if (!strcmp(cmsg.cmd, "DCC")
                       && p->conn_class->dcc_proxy_outgoing) {
              struct sockaddr_in vis_addr;
              int len;

              /* We need our local address to do anything DCC related */
              len = sizeof(struct sockaddr_in);
              if (getsockname(p->server_sock, (struct sockaddr *)&vis_addr,
                               &len)) {
                syscall_fail("getsockname", "", 0);

              } else if ((cmsg.numparams >= 4)
                         && (!irc_strcasecmp(cmsg.params[0], "CHAT")
                             || !irc_strcasecmp(cmsg.params[0], "SEND"))) {
                char *tmp, *ptr, *dccmsg, *rejmsg;
                struct in_addr l_addr, r_addr;
                int l_port, r_port, t_port;
                char *rest = 0;
                int type = 0;

                /* Find out what type of DCC request this is */
                if (!irc_strcasecmp(cmsg.params[0], "CHAT")) {
                  type = DCC_CHAT;
                } else if (!irc_strcasecmp(cmsg.params[0], "SEND")) {
                  if (p->conn_class->dcc_send_fast) {
                    type = DCC_SEND_FAST;
                  } else {
                    type = DCC_SEND_SIMPLE;
                  }
                }

                /* Check whether there's a tunnel port */
                t_port = 0;
                if (p->conn_class->dcc_tunnel_outgoing)
                  t_port = dns_portfromserv(p->conn_class->dcc_tunnel_outgoing);
                
                /* Eww, host order, how the hell does this even work
                   between machines of a different byte order? */
                if (!t_port) {
                  r_addr.s_addr = strtoul(cmsg.params[2], (char **)NULL, 10);
                  r_port = atoi(cmsg.params[3]);
                } else {
                  r_addr.s_addr = INADDR_LOOPBACK;
                  r_port = ntohs(t_port);
                }
                l_addr.s_addr = ntohl(vis_addr.sin_addr.s_addr);
                if (cmsg.numparams >= 5)
                  rest = cmsg.paramstarts[4];

                /* Strip out this CTCP from the message, replacing it in
                   a moment with dccmsg */
                tmp = x_sprintf("\001%s\001", unquoted);
                ptr = strstr(str, tmp);
                dccmsg = 0;

                /* Save this in case we need it later */
                rejmsg = x_sprintf(":%s NOTICE %s :\001DCC REJECT %s %s "
                                   "(%s: unable to proxy)",
                                   msg.params[0], p->nickname,
                                   cmsg.params[0], cmsg.params[1], PACKAGE);
 
                /* Set up a dcc proxy */
                if (ptr && !dccnet_new(type, p->conn_class->dcc_proxy_timeout,
                                       p->conn_class->dcc_proxy_ports,
                                       p->conn_class->dcc_proxy_ports_sz,
                                       &l_port, r_addr, r_port, 0, 0,
                                       DCCN_FUNCTION(_ircclient_send_dccreject),
                                       p, rejmsg)) {
                  dccmsg = x_sprintf("\001DCC %s %s %lu %u%s%s\001",
                                     cmsg.params[0], cmsg.params[1],
                                     l_addr.s_addr, l_port,
                                     (rest ? " " : ""), (rest ? rest : ""));

                  if (p->conn_class->log_events & IRC_LOG_CTCP)
                    irclog_notice(p, msg.params[0], p->servername,
                                  "Sent DCC %s Request to %s", cmsg.params[0],
                                  msg.params[0]);

                } else if (ptr) {
                  dccmsg = x_strdup("");
                  _ircclient_send_dccreject(p, rejmsg);
                }

                /* Don't need this now */
                free(rejmsg);

                /* Cut out the old CTCP and replace with dccmsg */
                if (ptr) {
                  char *oldstr;

                  *ptr = 0;
                  ptr += strlen(tmp);

                  oldstr = str;
                  str = x_sprintf("%s%s%s", oldstr, dccmsg, ptr);

                  free(oldstr);
                  free(dccmsg);
                }

                free(tmp);

              } else {
                /* Unknown DCC */
                debug("Unknown or Unimplemented DCC request - %s",
                      cmsg.params[0]);
              }

            } else {
              /* Unknown CTCP */
              if (p->conn_class->log_events & IRC_LOG_CTCP)
                irclog_notice(p, msg.params[0], p->servername,
                              "Sent CTCP %s to %s", cmsg.cmd, msg.params[0]);

            }

            ircprot_freectcp(&cmsg);
            free(unquoted);
          }

          /* Send str */
          if (strlen(str))
            net_send(p->server_sock, ":%s PRIVMSG %s :%s\r\n",
                     (msg.src.orig ? msg.src.orig : p->nickname),
                     msg.params[0], str);
          squelch = 1;
          free(str);
        }

        if (p->conn_class->idle_maxtime)
          ircserver_resetidle(p);

      } else if (!irc_strcasecmp(msg.cmd, "NOTICE")) {
        /* Notices from us get logged */
        if (msg.numparams >= 2) {
          char *str;

          ircprot_stripctcp(msg.params[1], &str, 0);

          if (str && strlen(str)) {
            if (p->conn_class->log_events & IRC_LOG_TEXT) {
              struct ircchannel *c;

              c = ircnet_fetchchannel(p, msg.params[0]);
              if (c) {
                char *tmp;

                tmp = x_sprintf("%s!%s@%s", p->nickname, p->username,
                                p->hostname);
                irclog_notice(p, msg.params[0], tmp, "%s", str);
                free(tmp);
              }
            }
          }
          free(str);
        }

        if (p->conn_class->idle_maxtime)
          ircserver_resetidle(p);
        squelch = 0;

      } else {
        squelch = 0;
      }

      /* Send command up to server? (We know there is one at this point) */
      if (!squelch)
        net_send(p->server_sock, "%s\r\n", msg.orig);

    } else if (irc_strcasecmp(msg.cmd, "DIRCPROXY")) {
      /* Command didn't (and won't be) handled.  We better stick to the
         RFC and send a RPL_TRYAGAIN back. */
      ircclient_send_numeric(p, 263, "%s :Please wait a while and try again.",
                             msg.cmd);
    }
   
    /* /DIRCPROXY can be used at *any* time, if it ever sends anything to the
       server it has to do it explicitly (no automatic sending) and has to
       check there is a server there */
    if (!irc_strcasecmp(msg.cmd, "DIRCPROXY")) {
      if (msg.numparams >= 1) {
        if (!irc_strcasecmp(msg.params[0], "RECALL")) {
          char *src, *filter;
          long start, lines;

          /* User wants to recall stuff from log files */
          src = filter = 0;
          start = -1;
          lines = 0;
          
          if (msg.numparams >= 4) {
            src = msg.params[1];
            start = atol(msg.params[2]);
            lines = atol(msg.params[3]);
          } else if (msg.numparams >= 3) {
            if (!irc_strcasecmp(msg.params[2], "ALL")) {
              src = msg.params[1];
              lines = -1;
            } else if (strspn(msg.params[1], "0123456789")
                       == strlen(msg.params[1])) {
              start = atol(msg.params[1]);
              lines = atol(msg.params[2]);
            } else {
              src = msg.params[1];
              lines = atol(msg.params[2]);
            }
          } else if (msg.numparams >= 2) {
            if (!irc_strcasecmp(msg.params[1], "ALL")) {
              lines = -1;
            } else {
              lines = atol(msg.params[1]);
            }
          } else {
            ircclient_send_numeric(p, 461, ":Not enough parameters");
          }

          if (src) {
            struct ircchannel *c;

            c = ircnet_fetchchannel(p, src);
            if (!c) {
              filter = src;
              src = p->nickname;
            }
          } else {
            src = p->nickname;
          }

          irclog_recall(p, src, start, lines, filter);

        } else if (p->conn_class->allow_persist
                   && !irc_strcasecmp(msg.params[0], "PERSIST")) {
          /* User wants a die_on_close proxy to persist */
          if (p->die_on_close) {
            if (p->conn_class->disconnect_on_detach) {
              /* Its die_on_close because of configuration, can't dedicate! */
              p->die_on_close = 0;
              ircnet_announce_dedicated(p);
            } else if (!ircnet_dedicate(p)) {
              /* Okay, it was inetd - we can dedicate this */
              p->die_on_close = 0;
            } else {
              ircclient_send_notice(p, "Could not persist");
            }
          } else {
            ircnet_announce_dedicated(p);
          }

        } else if (!irc_strcasecmp(msg.params[0], "DETACH")) {
          /* User wants to detach and can't be bothered to use /QUIT */
          ircnet_announce_status(p);
          ircclient_send_error(p, "Detached from %s %s", PACKAGE, VERSION);

          /* Optional AWAY message can be supplied */
          if ((msg.numparams >= 2) && strlen(msg.paramstarts[1])) {
            _ircclient_detach(p, msg.paramstarts[1]);
          } else {
            _ircclient_detach(p, 0);
          }
          ircprot_freemsg(&msg);
          return 0;

        } else if (!irc_strcasecmp(msg.params[0], "QUIT")) {
          /* User wants to detach and end their proxy session */

          if (IS_SERVER_READY(p)) {
            /* Optional QUIT message can be supplied */
            if ((msg.numparams >= 2) && strlen(msg.paramstarts[1])) {
              ircserver_send_command(p, "QUIT", ":%s", msg.paramstarts[1]);
            } else if (p->conn_class->quit_message) {
              ircserver_send_command(p, "QUIT", ":%s",
                                     p->conn_class->quit_message);
            } else {
              ircserver_send_command(p, "QUIT", ":Leaving IRC - %s %s",
                                     PACKAGE, VERSION);
            }
          }

          ircserver_close_sock(p);
          p->conn_class = 0;
          ircclient_close(p);
          ircprot_freemsg(&msg);
          return 0;

        } else if (!irc_strcasecmp(msg.params[0], "MOTD")) {
          /* Display message of the day file */
          _ircclient_motd(p);

        } else if (p->conn_class->allow_die
                   && !irc_strcasecmp(msg.params[0], "DIE")) {
          /* User wants to kill us :( */
          ircclient_send_notice(p, "I'm melting!");
          stop();

        } else if (p->conn_class->allow_users
                   && !irc_strcasecmp(msg.params[0], "USERS")) {
          struct ircconnclass *c;
          struct ircproxy *cp;
          int i;

          c = connclasses;
          i = 0;

          ircclient_send_notice(p, "Connection classes:");

          while (c) {
            cp = ircnet_fetchclass(c);
            if (!cp) {
              c = c->next;
              continue;
            }

            ircclient_send_notice(p, "-%s %2d. %s -> %s (%s)",
                                  (cp == p ? ">" : " "), ++i,
                                  cp->client_host ? cp->client_host : "(none)",
                                  cp->servername ? cp->servername : "(none)",
                                  cp->nickname ? cp->nickname : "no nickname");
            c = c->next;
          }

        } else if (p->conn_class->allow_kill
                   && !irc_strcasecmp(msg.params[0], "KILL")) {
          struct ircproxy *proxy;

          /* User wants to kill a user */
          if (msg.numparams >= 2) {
            struct ircconnclass *c;
            struct ircproxy *cp;
            int i;

            /* Check the user list */
            proxy = 0;
            c = connclasses;
            i = 0;
            while (c) {
              cp = ircnet_fetchclass(c);
              if (!cp) {
                c = c->next;
                continue;
              }

              if ((atoi(msg.params[1]) == ++i)
                  || (cp->client_host
                      && !irc_strcasecmp(msg.params[1], cp->client_host))
                  || (cp->servername
                      && !irc_strcasecmp(msg.params[1], cp->servername))
                  || (cp->nickname
                      && !irc_strcasecmp(msg.params[1], cp->nickname))) {
                proxy = cp;
                break;
              }

              c = c->next;
            }

            if (proxy && (proxy == p)) {
              ircclient_send_notice(p, "Use /DIRCPROXY QUIT to kill yourself");
            } else if (proxy) {
              if (IS_SERVER_READY(proxy)) {
                ircserver_send_command(proxy, "QUIT",
                                       ":Killed by adminstrator - %s %s",
                                       PACKAGE, VERSION);
              }
              if (IS_CLIENT_READY(proxy)) {
                ircclient_send_error(proxy, "Killed by administrator");
              }

              ircserver_close_sock(proxy);
              proxy->conn_class = 0;
              ircclient_close(proxy);

            } else {
              ircclient_send_numeric(p, 401, "No such user, "
                                     "use /DIRCPROXY USERS to see them");
            }

          } else {
            ircclient_send_numeric(p, 461, ":Not enough parameters");
          }

        } else if (!irc_strcasecmp(msg.params[0], "SERVERS")) {
          struct strlist *s;
          int i;

          s = p->conn_class->servers;
          i = 0;

          /* User wants a server list */
          if (s) {
            ircclient_send_notice(p, "You can connect to:");
          } else {
            ircclient_send_notice(p, "No servers");
          }

          while (s) {
            ircclient_send_notice(p, "-%s %2d. %s",
                                  (s == p->conn_class->next_server ? ">" : " "),
                                  ++i, s->str);
            s = s->next;
          }

        } else if (p->conn_class->allow_jump
                   && (!irc_strcasecmp(msg.params[0], "JUMP")
                       || !irc_strcasecmp(msg.params[0], "CONNECT"))) {
          struct strlist *server;

          /* User wants to jump to a new server */
          if (msg.numparams >= 2) {
            struct strlist *s;
            int i;

            /* Check the server list to see whether its a plain jump */
            server = 0;
            s = p->conn_class->servers;
            i = 0;
            while (s) {
              if ((atoi(msg.params[1]) == ++i)
                  || !irc_strcasecmp(msg.params[1], s->str)) {
                server = s;
                break;
              }

              s = s->next;
            }

          } else {
            /* User wants to jump to the next server */
            server = 0;
            if (p->conn_class->next_server)
              server = p->conn_class->next_server->next;
            if (!server)
              server = p->conn_class->servers;
          }

          /* Allocate new server if jump_new */
          if (!server && p->conn_class->allow_jump_new
              && (msg.numparams >= 2)) {
            debug("New server");

            server = (struct strlist *)malloc(sizeof(struct strlist));
            server->str = x_strdup(msg.params[1]);
            server->next = 0;

            if (p->conn_class->servers) {
              struct strlist *ss;

              ss = p->conn_class->servers;
              while (ss->next)
                ss = ss->next;

              ss->next = server;
            } else {
              p->conn_class->servers = server;
            }

          } else if (!server) {
            ircclient_send_numeric(p, 402, "No such server, "
                                   "use /DIRCPROXY SERVERS to see them");
          }

          if (server) {
            debug("Jumping to %s", server->str);

            p->conn_class->next_server = server;
            ircserver_connectagain(p);

            /* We have no server now, so need to get out of here */
            ircprot_freemsg(&msg);
            return 0;
          }

        } else if (p->conn_class->allow_host
                   && !irc_strcasecmp(msg.params[0], "HOST")) {
          /* User wants to change their hostname */
          free(p->conn_class->local_address);
          p->conn_class->local_address = 0;

          if (msg.numparams >= 2) {
            if (irc_strcasecmp(msg.params[1], "none"))
              p->conn_class->local_address = x_strdup(msg.params[1]);

          } else if (p->conn_class->orig_local_address) {
            p->conn_class->local_address =
                x_strdup(p->conn_class->orig_local_address);
          }

          ircserver_connectagain(p);

          /* We have no server now, so need to get out of here */
          ircprot_freemsg(&msg);
          return 0;

        } else if (!irc_strcasecmp(msg.params[0], "STATUS")) {
          struct ircchannel *c;
          struct strlist *s;

          ircclient_send_notice(p, "%s %s status:", PACKAGE, VERSION);
          ircclient_send_notice(p, "- Nickname on server: %s", p->nickname);
          ircclient_send_notice(p, "- Nickname to guard: %s", p->setnickname);
          ircclient_send_notice(p, "- Username for server: %s", p->username);
          ircclient_send_notice(p, "- Hostname for server: %s", p->hostname);
          ircclient_send_notice(p, "- Real name for server: %s", p->realname);
          ircclient_send_notice(p, "-");

          ircclient_send_notice(p, "- Client status: %s",
                                IS_CLIENT_READY(p) ? "Ready" : "");
          if (p->client_status != IRC_CLIENT_ACTIVE) {
            if (p->client_status & IRC_CLIENT_CONNECTED)
              ircclient_send_notice(p, "-   Connected");
            if (p->client_status & IRC_CLIENT_GOTPASS)
              ircclient_send_notice(p, "-   Received password");
            if (p->client_status & IRC_CLIENT_GOTNICK)
              ircclient_send_notice(p, "-   Received nickname");
            if (p->client_status & IRC_CLIENT_GOTUSER)
              ircclient_send_notice(p, "-   Received user information");
            if (p->client_status & IRC_CLIENT_AUTHED)
              ircclient_send_notice(p, "-   Authorised");
            if (p->client_status & IRC_CLIENT_SENTWELCOME)
              ircclient_send_notice(p, "-   Welcomed");
          }
          ircclient_send_notice(p, "-");

          ircclient_send_notice(p, "- Server status: %s",
                                IS_SERVER_READY(p) ? "Ready" : "");
          if (p->server_status != IRC_SERVER_ACTIVE) {
            if (p->server_status & IRC_SERVER_CREATED)
              ircclient_send_notice(p, "-   Created");
            if (p->server_status & IRC_SERVER_SEEN)
              ircclient_send_notice(p, "-   Seen");
            if (p->server_status & IRC_SERVER_CONNECTED)
              ircclient_send_notice(p, "-   Connected");
            if (p->server_status & IRC_SERVER_INTRODUCED)
              ircclient_send_notice(p, "-   Introduced ourselves");
            if (p->server_status & IRC_SERVER_GOTWELCOME)
              ircclient_send_notice(p, "-   Have been welcomed");
          }
          ircclient_send_notice(p, "-");

          ircclient_send_notice(p,  "- Servers.  Current marked by '->'");
          s = p->conn_class->servers;
          while (s) {
            ircclient_send_notice(p, "-%s  %s",
                                  (s == p->conn_class->next_server ? ">" : " "),
                                  s->str);
            s = s->next;
          }
          ircclient_send_notice(p, "-");

          ircclient_send_notice(p, "- Channels");
          c = p->channels;
          while (c) {
            ircclient_send_notice(p, "-   %s%s%s%s%s%s",
                                  c->name,
                                  c->key ? " (key: " : "",
                                  c->key ? c->key : "",
                                  c->key ? ")" : "",
                                  c->inactive ? " (removed by force)" : "",
                                  c->unjoined ? " (left on detach)" : "");
            c = c->next;
          }
          ircclient_send_notice(p, "-");

          ircclient_send_notice(p, "- Advanced:");
          ircclient_send_notice(p, "-   Allow MOTD count: %d", p->allow_motd);
          ircclient_send_notice(p, "-   Allow PONG count: %d", p->allow_pong);
          ircclient_send_notice(p, "-   411 Squelch count: %d", p->squelch_411);
          ircclient_send_notice(p, "-   Expecting NICK count: %d",
                                p->expecting_nick);

          if (p->squelch_modes)
            ircclient_send_notice(p, "-   Squelching mode changes:");
          s = p->squelch_modes;
          while (s) {
            ircclient_send_notice(p, "-     %s", s->str);
            s = s->next;
          }

        } else if (!irc_strcasecmp(msg.params[0], "HELP")) {
          /* User needs a little help */
          char **help_page;

          help_page = 0;

          if ((msg.numparams >= 2) && strlen(msg.params[1])) {
            if (!irc_strcasecmp(msg.params[1], "RECALL")) {
              help_page = help_recall;
            } else if (p->conn_class->allow_persist
                       && !irc_strcasecmp(msg.params[1], "PERSIST")) {
              help_page = help_persist;
            } else if (!irc_strcasecmp(msg.params[1], "DETACH")) {
              help_page = help_detach;
            } else if (!irc_strcasecmp(msg.params[1], "QUIT")) {
              help_page = help_quit;
            } else if (!irc_strcasecmp(msg.params[1], "MOTD")) {
              help_page = help_motd;
            } else if (p->conn_class->allow_die
                       && !irc_strcasecmp(msg.params[1], "DIE")) {
              help_page = help_die;
            } else if (!irc_strcasecmp(msg.params[1], "SERVERS")) {
              help_page = help_servers;
            } else if (p->conn_class->allow_jump
                       && !irc_strcasecmp(msg.params[1], "JUMP")) {
              help_page = (p->conn_class->allow_jump_new
                           ? help_jump_new : help_jump);
            } else if (p->conn_class->allow_host
                       && !irc_strcasecmp(msg.params[1], "HOST")) {
              help_page = help_host;
            } else if (!irc_strcasecmp(msg.params[1], "STATUS")) {
              help_page = help_status;
            } else if (!irc_strcasecmp(msg.params[1], "USERS")) {
              help_page = help_users;
            } else if (!irc_strcasecmp(msg.params[1], "KILL")) {
              help_page = help_kill;
            } else if (!irc_strcasecmp(msg.params[1], "HELP")) {
              help_page = help_help;
            } else {
              help_page = help_index;
            }

          } else {
            help_page = help_index;
          }

          if (help_page) {
            int i;

            i = 0;
            ircclient_send_notice(p, "%s %s help", PACKAGE, VERSION);
            while (help_page[i]) {
              ircclient_send_notice(p, "- %s", help_page[i]);
              i++;
            }
            
            if (help_page == help_index) {
              ircclient_send_notice(p, "-     HELP      "
                                   "(help on /dircproxy commands)");
              ircclient_send_notice(p, "-     MOTD      "
                                   "(show dircproxy message of the day)");
              ircclient_send_notice(p, "-     STATUS    "
                                    "(show dircproxy status information)");
              ircclient_send_notice(p, "-     RECALL    "
                                    "(recall text from log files)");
              ircclient_send_notice(p, "-     DETACH    "
                                    "(detach from dircproxy)");
              if (p->conn_class->allow_persist)
                ircclient_send_notice(p, "-     PERSIST   "
                                      "(keep session after detach)");
              ircclient_send_notice(p, "-     QUIT      "
                                    "(end dircproxy session)");
              if (p->conn_class->allow_die)
                ircclient_send_notice(p, "-     DIE       "
                                      "(terminate dircproxy)");
              if (p->conn_class->allow_users)
                ircclient_send_notice(p, "-     USERS     "
                                      "(show users using dircproxy)");
              if (p->conn_class->allow_kill)
                ircclient_send_notice(p, "-     KILL      "
                                      "(terminate a user's session)");
              ircclient_send_notice(p, "-     SERVERS   "
                                    "(show servers list)");
              if (p->conn_class->allow_jump)
                ircclient_send_notice(p, "-     JUMP      "
                                      "(jump to a different server)");
              if (p->conn_class->allow_host)
                ircclient_send_notice(p, "-     HOST      "
                                      "(change your visible hostname)");

              i = 0;
              while (help_index_end[i]) {
                ircclient_send_notice(p, "- %s", help_index_end[i]);
                i++;
              }
            }

            ircclient_send_notice(p, "-");
          }

        } else {
          /* Invalid command */
          ircclient_send_numeric(p, 421, "%s :Unknown DIRCPROXY command",
                                 msg.params[0]);

        }
      } else {
        ircclient_send_numeric(p, 461, ":Not enough parameters");
      }
    }
  }

  /* Do we have enough information to authenticate them? */
  if (!(p->client_status & IRC_CLIENT_AUTHED)
      && (p->client_status & IRC_CLIENT_GOTPASS)
      && (p->client_status & IRC_CLIENT_GOTNICK)
      && (p->client_status & IRC_CLIENT_GOTUSER))
  {
    _ircclient_authenticate(p, p->password);
    free(p->password);
    p->password = 0;
    p->client_status &= ~(IRC_CLIENT_GOTPASS);
  }

  /* Do we have enough information to connect to a server? */
  if (IS_CLIENT_READY(p) && !p->dead) {
    if (p->server_status != IRC_SERVER_ACTIVE) {
      if (!(p->server_status & IRC_SERVER_CREATED)) {
        if (p->conn_class && p->conn_class->server_autoconnect) {
          ircserver_connect(p);
        } else {
          ircclient_send_notice(p, "Please send /DIRCPROXY JUMP "
                                "<hostname>[:[port][:[password]]] to choose a "
                                "server");

          /* This won't delete an existing timer */
          timer_new((void *)p, "client_connect", g.connect_timeout,
                    TIMER_FUNCTION(_ircclient_timedout), (void *)1);
        }
      } else if (!IS_SERVER_READY(p)) {
        ircclient_send_notice(p, "Connection to server is in progress...");
      }
    } else if (!(p->client_status & IRC_CLIENT_SENTWELCOME)) {
      ircclient_welcome(p);
    }
  }

  ircprot_freemsg(&msg);
  return 0;
}

/* Got a password */
static int _ircclient_authenticate(struct ircproxy *p, const char *password) {
  struct ircconnclass *cc;

  cc = connclasses;
  while (cc) {
#ifdef ENCRYPTED_PASSWORDS
    char *cmp;

    cmp = crypt(password, cc->password);

    if (!strcmp(cc->password, cmp)) {
#else
    if (!strcmp(cc->password, password)) {
#endif
      if (cc->masklist) {
        struct strlist *m;
        char *ip;

        ip = inet_ntoa(p->client_addr.sin_addr);
        
        m = cc->masklist;
        while (m) {
          if (strcasematch(ip, m->str) || strcasematch(p->client_host, m->str))
            break;

          m = m->next;
        }

        /* We got a matching masklist, so this one's ok */
        if (m)
          break;
      } else {
        break;
      }
    }

    cc = cc->next;
  }

  if (cc) {
    struct ircproxy *tmp_p;

    tmp_p = ircnet_fetchclass(cc);
    if (tmp_p && (tmp_p->client_status & IRC_CLIENT_CONNECTED)) {
      if (tmp_p->conn_class->disconnect_existing) {
        debug("Already connected, disconnecting existing");

        ircclient_send_error(tmp_p, "Collided with new user");
        ircclient_close(tmp_p);

        if (tmp_p->dead) {
          debug("Kicked off client, and they died");
          tmp_p = 0;
        }
      } else {
        debug("Already connected, disconnecting incoming");
        ircclient_send_error(p, "Already connected");
        ircclient_close(p);
        return -1;
      }
    }

    /* Check again, in case killing existing user killed the proxy */
    if (tmp_p) {
      debug("Attaching new client to old server session");

      tmp_p->client_sock = p->client_sock;
      tmp_p->client_status |= IRC_CLIENT_CONNECTED | IRC_CLIENT_AUTHED;
      tmp_p->client_addr = p->client_addr;
      net_hook(tmp_p->client_sock, SOCK_NORMAL, (void *)tmp_p,
               ACTIVITY_FUNCTION(_ircclient_data),
               ERROR_FUNCTION(_ircclient_error));

      /* If the connecting client doesn't agree with the proxy about its
         nickname, then correct it. */
      if (strcmp(p->nickname, tmp_p->nickname))
        ircclient_send_selfcmd(p, "NICK", ":%s", tmp_p->nickname);

      /* If we've got to restore a different nickname, then do that now */
      if (tmp_p->oldnickname && strcmp(tmp_p->oldnickname, tmp_p->nickname))
        ircclient_change_nick(tmp_p, tmp_p->oldnickname);
      
      /* We don't need this anymore */
      free(tmp_p->oldnickname);
      tmp_p->oldnickname = 0;

      /* Unset any away message if we set one */
      if (!tmp_p->awaymessage && (tmp_p->server_status == IRC_SERVER_ACTIVE)
          && tmp_p->conn_class->away_message)
        ircserver_send_command(tmp_p, "AWAY", "");

      /* Rejoin any channels we parted */
      if ((tmp_p->server_status == IRC_SERVER_ACTIVE) && tmp_p->channels) {
        struct ircchannel *c;

        c = tmp_p->channels;
        while (c) {
          if (c->unjoined) {
            if (c->key) {
              ircserver_send_command(tmp_p, "JOIN", "%s :%s", c->name, c->key);
            } else {
              ircserver_send_command(tmp_p, "JOIN", ":%s", c->name);
            }
          }

          c = c->next;
        }
      }

      /* Send attach message to all channels we're on */
      if (tmp_p->server_status == IRC_SERVER_ACTIVE) {
        if (tmp_p->conn_class->attach_message) {
          struct ircchannel *c;
          int slashme;
          char *msg;

          msg = tmp_p->conn_class->attach_message;
          if ((strlen(msg) >= 5) && !strncasecmp(msg, "/me ", 4)) {
            /* Starts with /me */
            slashme = 1;
            msg += 4;
          } else {
            slashme = 0;
          }

          c = tmp_p->channels;
          while (c) {
            if (!c->inactive) {
              if (slashme) {
                ircserver_send_command(tmp_p, "PRIVMSG",
                                       "%s :\001ACTION %s\001", c->name, msg);
              } else {
                ircserver_send_command(tmp_p, "PRIVMSG", "%s :%s",
                                       c->name, msg);
              }
            }
            c = c->next;
          }
        }
      }
 
      if ((tmp_p->server_status == IRC_SERVER_ACTIVE)
          && !(tmp_p->client_status & IRC_CLIENT_SENTWELCOME))
        ircclient_welcome(tmp_p);

      p->client_status = IRC_CLIENT_NONE;
      p->client_sock = -1;
      p->dead = 1;

    } else {
      struct strlist *s;

      p->conn_class = cc;
      p->client_status |= IRC_CLIENT_AUTHED;
      time(&(p->start));

      if (p->conn_class->disconnect_on_detach)
        p->die_on_close = 1;

      /* Okay, they've authed for the first time, make the log directory
         here */
      if (p->conn_class->other_log_enabled || p->conn_class->chan_log_enabled) {
        if (irclog_maketempdir(p))
          ircclient_send_notice(p, "(warning) Unable to create log "
                                   "directory, logging disabled");
      }

      /* Initialise the server/private message log */
      irclog_init(p, "");

      /* Open a log file if we're always logging */
      if (p->conn_class->other_log_enabled && p->conn_class->other_log_always) {
        if (irclog_open(p, ""))
          ircclient_send_notice(p, "(warning) Unable to log server/private "
                                   "messages");
      }

      /* Join initial channels */
      s = p->conn_class->channels;
      while (s) {
        struct ircchannel *c;
        char *name, *key;

        name = x_strdup(s->str);
        key = strchr(name, ' ');
        if (key)
          *(key++) = 0;

        ircnet_addchannel(p, name);
        c = ircnet_fetchchannel(p, name);
        if (c) {
          c->inactive = 1;
          if (key)
            c->key = x_strdup(key);
        }

        free(name);

        s = s->next;
      }

      /* Set initial modes */
      if (p->conn_class->initial_modes)
        ircclient_change_mode(p, p->conn_class->initial_modes);
    }

    return 0;
  }

  ircclient_send_numeric(p, 464, ":You are not permitted to use this proxy");
  ircclient_send_error(p, "Permission Denied");
  ircclient_close(p);
  return -1;
}

/* Request a nickname change */
int ircclient_change_nick(struct ircproxy *p, const char *newnick) {
  /* If a server is ready to accept a NICK command, send it */
  if (IS_SERVER_READY(p)) {
    debug("Requesting nick change from '%s' to '%s'",
          (p->nickname ? p->nickname : ""), newnick);
    ircserver_send_command(p, "NICK", ":%s", newnick);
  }

  /* If we have a nickname already then the server will confirm that, otherwise
     we should remember it ourselves */
  if (p->client_status & IRC_CLIENT_GOTNICK) {
    debug("Server will change it for us");
    p->expecting_nick = 1;

    return 0;
  } else {
    int ret;
    
    /* Because we're not expecting a server confirmation, then we better
       do the confirm for the client ourselves */
    if ((p->client_status & IRC_CLIENT_CONNECTED) &&
        (p->client_status & IRC_CLIENT_AUTHED))
      ircclient_send_selfcmd(p, "NICK", ":%s", newnick);

    /* Make the change in the wings */
    ret = ircclient_nick_changed(p, newnick);
    ircclient_setnickname(p);
    ircclient_checknickname(p);
    
    return ret;
  }
}

/* Nickname has now definitly been changed */
int ircclient_nick_changed(struct ircproxy *p, const char *newnick) {
  if (p->nickname)
    debug("nickname WAS '%s'", p->nickname);
  free(p->nickname);

  p->nickname = x_strdup(newnick);
  p->client_status |= IRC_CLIENT_GOTNICK;
  debug("nickname NOW '%s'", p->nickname);

  return 0;
}

/* Make the current nickname the set one */
int ircclient_setnickname(struct ircproxy *p) {
  /* Update setnickname too */
  if (p->setnickname)
    free(p->setnickname);
  p->setnickname = x_strdup(p->nickname);
  debug("Changed setnickname to '%s'", p->setnickname);

  return 0;
}

/* Check whether we need to restore the nickname later */
int ircclient_checknickname(struct ircproxy *p) {
  if (p->conn_class && p->conn_class->nick_keep
      && strcmp(p->nickname, p->setnickname))
    timer_new((void *)p, "client_resetnick", NICK_GUARD_TIME,
              TIMER_FUNCTION(_ircclient_resetnick), (void *)0);

  return 0;
}

/* Change the nickname to something we generate ourselves */
int ircclient_generate_nick(struct ircproxy *p, const char *tried) {
  char *c, *nick;
  int ret;

  c = nick = (char *)malloc(strlen(tried) + 2);
  strcpy(nick, tried);
  c += strlen(nick) - 1;

  /* We add -'s until we can't, then we move back through them cycling them
     0..9 then finally _ until the whole nickname is _________.  Once that
     happens we just use 'dircproxy' and do it all over again */
  if (strlen(nick) < 9) {
    *(++c) = '-';
    *(++c) = 0;
  } else {
    while (c >= nick) {
      if (*c == '-') {
        *c = '0';
        break;
      } else if ((*c >= '0') && (*c < '9')) {
        (*c)++;
        break;
      } else if (*c == '9') {
        *c = '_';
        break;
      } else if (*c == '_') {
        c--;
      } else {
        *c = '-';
        break;
      }
    }

    if (c < nick) {
      free(nick);
      nick = x_strdup(FALLBACK_NICKNAME);
    }
  }

  /* Ask the server to change the nickname */
  if (IS_SERVER_READY(p)) {
    debug("Requesting nick change from '%s' to '%s'",
          (p->nickname ? p->nickname : ""), nick);
    ircserver_send_command(p, "NICK", ":%s", nick);
  }

  /* If we don't have a nickname yet, make the change ourselves */
  if (!(p->client_status & IRC_CLIENT_GOTNICK)) {
    /* We know that there is no client connected, otherwise this would
       never have been called, so no point sending a nickname to the client.

       Just change it in our memory */
    ret = ircclient_nick_changed(p, nick);
    ircclient_checknickname(p);
  } else {
    debug("Server will change it for us");
  }
  
  free(nick);
  return 0;
}

/* Timer hook to restore a lost nickname */
static void _ircclient_resetnick(struct ircproxy *p, void *data) {
  /* We don't have a server anymore, setnickname will be restored on
     connection attempt */
  if (!IS_SERVER_READY(p))
    return;

  /* Is it worth doing this? */
  if (!strcmp(p->nickname, p->setnickname))
    return;
  
  /* Ask the server to change the nickname */
  debug("Attempting to restore nickname to '%s'", p->setnickname);
  ircclient_change_nick(p, p->setnickname);
}

/* Got some details */
static int _ircclient_got_details(struct ircproxy *p, const char *newusername,
                                  const char *newmode, const char *unused,
                                  const char *newrealname) {
  int mode;

  if (!p->username)
    p->username = x_strdup(newusername);

  if (!p->realname)
    p->realname = x_strdup(newrealname);

  /* RFC2812 states that the second parameter to USER is a numeric stating
     what default modes to set.  This disagrees with RFC1459.  We follow
     the newer one if we can, as the old hostname/servername combo were
     useless and ignored anyway. */
  mode = atoi(newmode);
  if (mode & RFC2812_MODE_W)
    ircclient_change_mode(p, "+w");
  if (mode & RFC2812_MODE_I)
    ircclient_change_mode(p, "+w");

  /* Okay we have the username now */
  p->client_status |= IRC_CLIENT_GOTUSER;

  return 0;
}

/* Got a personal mode change */
int ircclient_change_mode(struct ircproxy *p, const char *change) {
  char *ptr, *str;
  int add = 1;

  ptr = str = x_strdup(change);
  debug("Mode change from '%s', '%s'", (p->modes ? p->modes : ""), str);

  while (*ptr) {
    switch (*ptr) {
      case '+':
        add = 1;
        break;
      case '-':
        add = 0;
        break;
      default:
        if (add) {
          if (!p->modes || !strchr(p->modes, *ptr)) {
            if (p->modes) {
              p->modes = (char *)realloc(p->modes, strlen(p->modes) + 2);
            } else {
              p->modes = (char *)malloc(2);
              p->modes[0] = 0;
            }
            p->modes[strlen(p->modes) + 1] = 0;
            p->modes[strlen(p->modes)] = *ptr;
          }
        } else if (p->modes) {
          char *pos;

          pos = strchr(p->modes, *ptr);
          if (pos) {
            char *tmp;

            tmp = p->modes;
            p->modes = (char *)malloc(strlen(p->modes));
            *(pos++) = 0;
            strcpy(p->modes, tmp);
            strcpy(p->modes + strlen(p->modes), pos);
            free(tmp);

            if (!strlen(p->modes)) {
              free(p->modes);
              p->modes = 0;
            } 
          }
        }
    }

    ptr++;
  }

  debug("    now '%s'", (p->modes ? p->modes : ""));
  free(str);
  return 0;
}

/* Close the client socket */
int ircclient_close(struct ircproxy *p) {
  timer_del((void *)p, "client_auth");
  timer_del((void *)p, "client_connect");

  net_close(&(p->client_sock));
  p->client_sock = -1;
  p->client_status &= ~(IRC_CLIENT_CONNECTED | IRC_CLIENT_AUTHED
                        | IRC_CLIENT_SENTWELCOME);

  /* No connection class, or no nick or user? Die! */
  if (!p->conn_class || !(p->client_status & IRC_CLIENT_GOTNICK)
      || !(p->client_status & IRC_CLIENT_GOTUSER)) {
    if (p->server_status & IRC_SERVER_CREATED) {
      ircserver_send_command(p, "QUIT", ":I shouldn't really be here - %s %s",
                             PACKAGE, VERSION);
      ircserver_close_sock(p); 
    }
    p->dead = 1;
  }

  return p->dead;
}

/* send message of the day to the user */
static int _ircclient_motd(struct ircproxy *p) {
  FILE *motd_file;

  if (p->conn_class->motd_file) {
    motd_file = fopen(p->conn_class->motd_file, "r");
    if (!motd_file)
      syscall_fail("fopen", p->conn_class->motd_file, 0);
  } else {
    motd_file = (FILE *)0;
  }

  /* Check whether to do anything, and send appropriate numerics */
  if (!p->conn_class->motd_logo && !p->conn_class->motd_stats && !motd_file) {
    if (p->conn_class->motd_file) {
      ircclient_send_numeric(p, 422, ":MOTD File is missing");
    } else {
      ircclient_send_numeric(p, 422, ":No MOTD");
    }
    return 0;
  } else {
    ircclient_send_numeric(p, 375, ":- %s Message of the Day -", PACKAGE);
  }

  /* Send the pretty dircproxy logo */
  if (p->conn_class->motd_logo) {
    char *ver;
    int line;
   
    line = 0;
    while (logo[line]) {
      ircclient_send_numeric(p, 372, ":- %s", logo[line]);
      line++;
    }

    ver = x_sprintf(verstr, VERSION);
    ircclient_send_numeric(p, 372, ":- %s", ver);
    ircclient_send_numeric(p, 372, ":-");
    free(ver);
  }

  /* Send from file */
  if (motd_file) {
    char buff[512];

    while (fgets(buff, 512, motd_file)) {
      char *ptr;

      ptr = buff + strlen(buff);
      while ((ptr >= buff) && (!ptr || strchr(" \t\r\n", *ptr))) *(ptr--) = 0;
      ircclient_send_numeric(p, 372, ":- %s", buff);
    }

    ircclient_send_numeric(p, 372, ":-");
  }

  /* Send some stats */
  if (p->conn_class->motd_stats) {
    /* Server/private messages */
    if (p->other_log.filename) {
      char *s;

      if (p->conn_class->other_log_recall == -1) {
        s = x_strdup(p->other_log.nlines ? "all" : "none");
      } else if (p->conn_class->other_log_recall == 0) {
        s = x_strdup("none");
      } else if (p->conn_class->other_log_recall == p->other_log.nlines) {
        s = x_strdup("all");
      } else {
        s = x_sprintf("%ld", p->conn_class->other_log_recall);
      }

      ircclient_send_numeric(p, 372, ":- %ld server/private message%s "
                             "(%s will be sent)", p->other_log.nlines,
                             (p->other_log.nlines == 1 ? "" : "s"), s);
      ircclient_send_numeric(p, 372, ":-");

      free(s);
    }

    /* Channels they were on */
    if (p->channels) {
      struct ircchannel *c;

      c = p->channels;
      while (c) {
        if (c->inactive) {
          if (c->log.nlines) {
            ircclient_send_numeric(p, 372, ":- was on %s but removed by force",
                                   c->name);
          } else {
            ircclient_send_numeric(p, 372, ":- yet to join %s", c->name);
          }
        } else if (c->unjoined) {
          ircclient_send_numeric(p, 372, ":- was on %s, yet to rejoin",
                                 c->name);
        } else if (c->log.filename) {
          char *s;

          if (p->conn_class->chan_log_recall == -1) {
            s = x_strdup(p->other_log.nlines ? "all" : "none");
          } else if (p->conn_class->chan_log_recall == 0) {
            s = x_strdup("none");
          } else if (p->conn_class->chan_log_recall == c->log.nlines) {
            s = x_strdup("all");
          } else {
            s = x_sprintf("%ld", MIN(c->log.nlines,
                                     p->conn_class->chan_log_recall));
          }

          ircclient_send_numeric(p, 372, ":- %s. %ld line%s logged. "
                                 "(%s will be sent)", c->name, c->log.nlines,
                                 (c->log.nlines == 1 ? "" : "s"), s);

          free(s);
        } else {
          ircclient_send_numeric(p, 372, ":- %s (not logged)", c->name);
        }
        c = c->next;
      }
      ircclient_send_numeric(p, 372, ":-");
    }
  }

  /* Done */
  ircclient_send_numeric(p, 376, ":End of /MOTD command");
  if (motd_file)
    fclose(motd_file);

  return 1;
}

/* send welcome headers to the user */
int ircclient_welcome(struct ircproxy *p) {
  char tbuf[40];

  strftime(tbuf, sizeof(tbuf), START_TIMEDATE_FORMAT, localtime(&(p->start)));

  ircclient_send_numeric(p, 1, ":Welcome to the Internet Relay Network %s",
                         p->nickname);
  ircclient_send_numeric(p, 2, ":Your host is %s running %s via %s %s",
                         p->servername,
                         (p->serverver ? p->serverver : "(unknown)"),
                         PACKAGE, VERSION);
  ircclient_send_numeric(p, 3, ":This proxy has been running since %s", tbuf);
  if (p->serverver)
    ircclient_send_numeric(p, 4, "%s %s %s %s",
                           p->servername, p->serverver,
                           p->serverumodes, p->servercmodes);

  _ircclient_motd(p);

  if (p->modes)
    ircclient_send_selfcmd(p, "MODE", "%s +%s", p->nickname, p->modes);

  if (p->awaymessage) {
    /* Ack.  There's no reason for a client to expect AWAY from a server,
       so we cheat and send a 306, reminding them what their away message
       was in the text.  This might not trick the client either, but hey,
       I can't do anything about that. */
    ircclient_send_numeric(p, 306, ":%s: %s",
                           "You left yourself away.  Your message was",
                           p->awaymessage);
  }

  if (p->channels) {
    struct ircchannel *c;

    c = p->channels;
    while (c) {
      if (!c->inactive && !c->unjoined) {
        ircclient_send_selfcmd(p, "JOIN", ":%s", c->name);
        ircserver_send_command(p, "TOPIC", ":%s", c->name);
        ircserver_send_command(p, "NAMES", ":%s", c->name);

        if (p->conn_class->chan_log_enabled) {
          irclog_autorecall(p, c->name);
          if (!p->conn_class->chan_log_always)
            irclog_close(p, c->name);
        }
      }

      c = c->next;
    }
  }

  /* Recall other log file */
  if (p->conn_class->other_log_enabled) {
    irclog_autorecall(p, p->nickname);
    if (!p->conn_class->other_log_always)
      irclog_close(p, p->nickname);
  }

  if (p->conn_class->log_events & IRC_LOG_CLIENT)
    irclog_notice(p, 0, PACKAGE, "You connected");
  ircnet_announce_status(p);

  p->client_status |= IRC_CLIENT_SENTWELCOME;
  return 0;
}

/* Timer hook when something's timed out */
static void _ircclient_timedout(struct ircproxy *p, void *data) {
  int connect;

  /* These are always called after the timeout if the client's still
     connected, check the event they were looking for has happened */
  connect = (int)data;
  if (connect && (p->server_status & IRC_SERVER_CREATED)) {
    /* Connecting to server, and a socket has been created to do it */
    debug("Server has been chosen");
    return;
  } else if (!connect && IS_CLIENT_READY(p)) {
    /* Authorization, and client is ready to accept data */
    debug("They are authorized");
    return;
  }

  /* Timeout! */
  debug("Timed out");
  ircclient_send_error(p, "%s Timeout", (connect ? "Connect" : "Login"));
  ircclient_close(p);
}

/* send a numeric to the user */
int ircclient_send_numeric(struct ircproxy *p, short numeric,
                           const char *format, ...) {
  va_list ap;
  char *msg;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  ret = net_send(p->client_sock, ":%s %03d %s %s\r\n",
                 (p->servername ? p->servername : PACKAGE), numeric,
                 (p->nickname ? p->nickname : "*"), msg);
  debug("<- ':%s %03d %s %s'", (p->servername ? p->servername : PACKAGE),
        numeric, (p->nickname ? p->nickname : "*"), msg);

  free(msg);
  return ret;
}

/* send a notice to the user */
int ircclient_send_notice(struct ircproxy *p, const char *format, ...) {
  va_list ap;
  char *msg;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  ret = net_send(p->client_sock, ":%s %s %s :%s\r\n", PACKAGE, "NOTICE",
                 (p->nickname ? p->nickname : "AUTH"), msg);
  debug("<- ':%s %s %s :%s'", PACKAGE, "NOTICE",
        (p->nickname ? p->nickname : "AUTH"), msg);

  free(msg);
  return ret;
}

/* send a notice to a channel */
int ircclient_send_channotice(struct ircproxy *p, const char *channel,
                              const char *format, ...) {
  va_list ap;
  char *msg;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  ret = net_send(p->client_sock, ":%s %s %s :%s\r\n",
                 (p->servername ? p->servername : PACKAGE), "NOTICE",
                 channel, msg);
  debug("<- ':%s %s %s :%s'", (p->servername ? p->servername : PACKAGE),
        "NOTICE", channel, msg);

  free(msg);
  return ret;
}

/* send a command to the user from the server */
int ircclient_send_command(struct ircproxy *p, const char *command,
                           const char *format, ...) {
  va_list ap;
  char *msg;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  ret = net_send(p->client_sock, ":%s %s %s\r\n",
                 (p->servername ? p->servername : PACKAGE), command, msg);
  debug("<- ':%s %s %s'", (p->servername ? p->servername : PACKAGE),
        command, msg);

  free(msg);
  return ret;
}

/* send a command to the user making it look like its from them */
int ircclient_send_selfcmd(struct ircproxy *p, const char *command,
                           const char *format, ...) {
  char *msg, *prefix;
  va_list ap;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  if (p->nickname && p->username && p->hostname) {
    prefix = x_sprintf(":%s!%s@%s ", p->nickname, p->username, p->hostname);
  } else if (p->nickname) {
    prefix = x_sprintf(":%s ", p->nickname);
  } else {
    prefix = (char *)malloc(1);
    prefix[0] = 0;
  }

  ret = net_send(p->client_sock, "%s%s %s\r\n", prefix, command, msg);
  debug("<- '%s%s %s'", prefix, command, msg);

  free(prefix);
  free(msg);
  return ret;
}

/* send an error to the user */
int ircclient_send_error(struct ircproxy *p, const char *format, ...) {
  char *msg, *nick, *user, *host;
  va_list ap;
  int ret;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  nick = p->nickname ? p->nickname : "";
  user = p->username ? p->username : "user";
  host = p->hostname ? p->hostname : "host";

  ret = net_send(p->client_sock, "%s :%s: %s[%s@%s] (%s)\r\n",
                 "ERROR", "Closing Link", nick, user, host, msg);
  debug("<- '%s :%s: %s[%s@%s] (%s)'", "ERROR", "Closing Link",
        nick, user, host, msg);

  free(msg);
  return ret;
}

/* Send a DCC reject message */
static int _ircclient_send_dccreject(struct ircproxy *p, const char *msg) {
  int ret = 1;

  if (p && p->conn_class && p->conn_class->dcc_proxy_sendreject &&
      (p->client_status == IRC_CLIENT_ACTIVE)) {
    ret = net_send(p->client_sock, "%s\r\n", msg);
    debug("<- '%s'", msg);
  }

  return ret;
}


syntax highlighted by Code2HTML, v. 0.9.1