/** * main.C: This file contains the main() routine. * * Copyright © 2007 Pippijn van Steenhoven / The Ermyth Team * Rights to this code are as documented in COPYING. * * * Portions of this file were derived from sources bearing the following license: * Copyright © 2005 Atheme Development Group * Rights to this code are documented in doc/pod/license.pod. * * $Id: main.C,v 1.10 2007/09/22 14:27:30 pippijn Exp $ */ #include "atheme.h" #include #include "confparse.h" #include #include #include #include static char const rcsid[] = "$Id: main.C,v 1.10 2007/09/22 14:27:30 pippijn Exp $"; REGISTER_MODULE ("saslserv/main", false, "The Ermyth Team "); list_t sessions; list_t sasl_mechanisms; /* main services client routine */ static void saslserv (sourceinfo_t *si, int parc, char *parv[]) { char *cmd; char *text; char orig[BUFSIZE]; /* this should never happen */ if (parv[0][0] == '&') { slog (LG_ERROR, "services(): got parv with local channel: %s", parv[0]); return; } /* make a copy of the original for debugging */ strlcpy (orig, parv[parc - 1], BUFSIZE); /* lets go through this to get the command */ cmd = strtok (parv[parc - 1], " "); text = strtok (NULL, ""); if (!cmd) return; if (*cmd == '\001') { handle_ctcp_common (si, cmd, text); return; } command_fail (si, fault::noprivs, "This service exists to identify " "connecting clients to the network. It has no " "public interface."); } static void on_config_ready (void) { if (saslsvs.me) del_service (saslsvs.me); saslsvs.me = add_service (saslsvs.nick, saslsvs.user, saslsvs.host, saslsvs.real, saslserv); } /* * Begin SASL-specific code */ static void sasl_logcommand (sasl_session_t *p, myuser_t *login, int level, char const * const fmt, ...) { va_list args; char lbuf[BUFSIZE]; va_start (args, fmt); vsnprintf (lbuf, BUFSIZE, fmt, args); slog (level, "%s %s:%s %s", saslsvs.nick, login ? login->name : "", p->uid, lbuf); va_end (args); } /* find an existing session by uid */ static sasl_session_t * find_session (char const * const uid) { sasl_session_t *p; node_t *n; LIST_FOREACH (n, sessions.head) { p = static_cast (n->data); if (!strcmp (p->uid, uid)) return p; } return NULL; } /* create a new session if it does not already exist */ static sasl_session_t * make_session (char const * const uid) { sasl_session_t *p = find_session (uid); node_t *n; if (p) return p; p = new sasl_session_t; strlcpy (p->uid, uid, IDLEN); n = node_create (); node_add (p, n, &sessions); return p; } /* free a session and all its contents */ static void destroy_session (sasl_session_t *p) { node_t *n, *tn; myuser_t *mu; if (p->flags & ASASL_NEED_LOG && p->username != NULL) { mu = myuser_t::find (p->username); if (mu != NULL) sasl_logcommand (p, mu, CMDLOG_LOGIN, "LOGIN (session timed out)"); } LIST_FOREACH_SAFE (n, tn, sessions.head) { if (n->data == p) { node_del (n, &sessions); node_free (n); } } sfree (p->buf); p->buf = p->p = NULL; if (p->mechptr) p->mechptr->mech_finish (p); /* Free up any mechanism data */ p->mechptr = NULL; /* We're not freeing the mechanism, just "dereferencing" it */ sfree (p->username); delete p; } /* find a mechanism by name */ static sasl_mechanism_t * find_mechanism (char *name) { node_t *n; sasl_mechanism_t *mptr; LIST_FOREACH (n, sasl_mechanisms.head) { mptr = static_cast (n->data); if (!strcmp (mptr->name, name)) return mptr; } slog (LG_DEBUG, "find_mechanism(): cannot find mechanism `%s'!", name); return NULL; } /* output an arbitrary amount of data to the SASL client */ static void sasl_write (char *target, char *data, int length) { char out[401]; int last = 400, rem = length; while (rem) { int nbytes = rem > 400 ? 400 : rem; memcpy (out, data, nbytes); out[nbytes] = '\0'; phandler->sasl_sts (target, 'C', out); data += nbytes; rem -= nbytes; last = nbytes; } /* The end of a packet is indicated by a string not of length 400. * If last piece is exactly 400 in size, send an empty string to * finish the transaction. * Also if there is no data at all. */ if (last == 400) phandler->sasl_sts (target, 'C', "+"); } /* authenticated, now double check that their account is ok for login */ static int login_user (sasl_session_t *p) { myuser_t *mu = myuser_t::find (p->username); metadata *md; if (mu == NULL) /* WTF? */ return 0; if ((md = mu->find_metadata ("private:freeze:freezer"))) { sasl_logcommand (p, NULL, CMDLOG_LOGIN, "failed LOGIN to %s (frozen)", mu->name); return 0; } if (mu->logins.size () >= me.maxlogins) { sasl_logcommand (p, NULL, CMDLOG_LOGIN, "failed LOGIN to %s (too many logins)", mu->name); return 0; } /* Log it with the full n!u@h later */ p->flags |= ASASL_NEED_LOG; return 1; } /* given an entire sasl message, advance session by passing data to mechanism * and feeding returned data back to client. */ static void sasl_packet (sasl_session_t *p, char *buf, int len) { int rc; size_t tlen = 0; char const *cloak; char *out = NULL; char *temp; char mech[21]; int out_len = 0; metadata *md; base64::encoder b64enc; base64::decoder b64dec; /* First piece of data in a session is the name of * the SASL mechanism that will be used. */ if (!p->mechptr) { if (len > 20) { phandler->sasl_sts (p->uid, 'D', "F"); destroy_session (p); return; } memcpy (mech, buf, len); mech[len] = '\0'; if (!(p->mechptr = find_mechanism (mech))) { /* Generate a list of supported mechanisms (disabled since charybdis doesn't support this yet). */ #if 0 char temp[400], *ptr = temp; int l = 0; node_t *n; LIST_FOREACH(n, sasl_mechanisms.head) { sasl_mechanism_t *mptr = n->data; if(l + strlen(mptr->name) > 510) break; strcpy(ptr, mptr->name); ptr += strlen(mptr->name); *ptr++ = ','; l += strlen(mptr->name) + 1; } if(l) ptr--; *ptr = '\0'; phandler->sasl_sts(p->uid, 'M', temp); #endif phandler->sasl_sts (p->uid, 'D', "F"); destroy_session (p); return; } rc = p->mechptr->mech_start (p, &out, &out_len); } else { if (b64dec.decode (buf, len, &temp, &tlen)) { rc = p->mechptr->mech_step (p, temp, tlen, &out, &out_len); delete[] temp; } else rc = ASASL_FAIL; } if (rc == ASASL_DONE) { myuser_t *mu = myuser_t::find (p->username); if (mu && login_user (p)) { if ((md = mu->find_metadata ("private:usercloak"))) cloak = md->value; else cloak = "*"; phandler->svslogin_sts (p->uid, "*", "*", cloak, mu->name); phandler->sasl_sts (p->uid, 'D', "S"); } else phandler->sasl_sts (p->uid, 'D', "F"); /* Will destroy session on introduction of user to net. */ return; } else if (rc == ASASL_MORE) { if (out_len) { if (b64enc.encode (out, out_len, &temp)) { sasl_write (p->uid, temp, strlen (temp)); delete[] temp; sfree (out); return; } } else { phandler->sasl_sts (p->uid, 'C', "+"); sfree (out); return; } } sfree (out); phandler->sasl_sts (p->uid, 'D', "F"); destroy_session (p); } /* interpret an AUTHENTICATE message */ static void sasl_input (char const * const uid, char const mode, char const * const buf) { sasl_session_t *p = make_session (uid); int len = strlen (buf); /* Abort packets, or maybe some other kind of (D)one */ if (mode == 'D') { destroy_session (p); return; } if (mode != 'S' && mode != 'C') return; if (p->buf == NULL) { p->buf = salloc (len + 1); p->p = p->buf; p->len = len; } else { if (p->len + len + 1 > 8192) /* This is a little much... */ { phandler->sasl_sts (p->uid, 'D', "F"); destroy_session (p); return; } p->buf = (char *) realloc (p->buf, p->len + len + 1); p->p = p->buf + p->len; p->len += len; } memcpy (p->p, buf, len); /* Messages not exactly 400 bytes are the end of a packet. */ if (len < 400) { p->buf[p->len] = '\0'; sasl_packet (p, p->buf, p->len); sfree (p->buf); p->buf = p->p = NULL; p->len = 0; } } /* clean up after a user who is finally on the net */ static void sasl_newuser (user_t *u) { sasl_session_t *p = find_session (u->uid); metadata *md_failnum; char lau[BUFSIZE], lao[BUFSIZE]; char strfbuf[BUFSIZE]; struct tm tm; myuser_t *mu; /* Not concerned unless it's a SASL login. */ if (p == NULL) return; /* We will log it ourselves, if needed */ p->flags &= ~ASASL_NEED_LOG; /* Find the account */ mu = p->username ? myuser_t::find (p->username) : NULL; if (mu == NULL) { notice (saslsvs.nick, u->nick, "Account %s dropped, login cancelled", p->username ? p->username : "??"); destroy_session (p); /* We'll remove their ircd login in handle_burstlogin() */ return; } destroy_session (p); if (is_soper (mu)) { snoop ("SOPER: \2%s\2 as \2%s\2", u->nick, mu->name); } mu->notice (saslsvs.nick, "%s!%s@%s has just authenticated as you (%s)", u->nick, u->user, u->vhost, mu->name); u->myuser = mu; mu->logins.insert (u); /* keep track of login address for users */ strlcpy (lau, u->user, BUFSIZE); strlcat (lau, "@", BUFSIZE); strlcat (lau, u->vhost, BUFSIZE); mu->add_metadata ("private:host:vhost", lau); /* and for opers */ strlcpy (lao, u->user, BUFSIZE); strlcat (lao, "@", BUFSIZE); /* Hack for charybdis before 2.1: store IP instead of vhost * (real host is not known at this time) -- jilles */ slog (LG_DEBUG, "nick %s host %s vhost %s ip %s", u->nick, u->host, u->vhost, u->ip); if (!strcmp (u->host, u->vhost) && *u->ip != '\0' && mu->find_metadata ("private:usercloak")) strlcat (lao, u->ip, BUFSIZE); else strlcat (lao, u->host, BUFSIZE); mu->add_metadata ("private:host:actual", lao); logcommand_user (saslsvs.me, u, CMDLOG_LOGIN, "LOGIN"); /* check for failed attempts and let them know */ if ((md_failnum = mu->find_metadata ("private:loginfail:failnum")) && (atoi (md_failnum->value) > 0)) { metadata *md_failtime, *md_failaddr; time_t ts; tm = *localtime (&mu->lastlogin); strftime (strfbuf, sizeof (strfbuf) - 1, "%b %d %H:%M:%S %Y", &tm); notice (saslsvs.nick, u->nick, "\2%d\2 failed %s since %s.", atoi (md_failnum->value), (atoi (md_failnum->value) == 1) ? "login" : "logins", strfbuf); md_failtime = mu->find_metadata ("private:loginfail:lastfailtime"); ts = atol (md_failtime->value); md_failaddr = mu->find_metadata ("private:loginfail:lastfailaddr"); tm = *localtime (&ts); strftime (strfbuf, sizeof (strfbuf) - 1, "%b %d %H:%M:%S %Y", &tm); notice (saslsvs.nick, u->nick, "Last failed attempt from: \2%s\2 on %s.", md_failaddr->value, strfbuf); mu->del_metadata ("private:loginfail:failnum"); /* md_failnum now invalid */ mu->del_metadata ("private:loginfail:lastfailtime"); mu->del_metadata ("private:loginfail:lastfailaddr"); } mu->lastlogin = NOW; u->callback.identify (u); } /* This function is run approximately once every 30 seconds. * It looks for flagged sessions, and deletes them, while * flagging all the others. This way stale sessions are deleted * after no more than 60 seconds. */ static void delete_stale (void *vptr) { sasl_session_t *p; node_t *n, *tn; LIST_FOREACH_SAFE (n, tn, sessions.head) { p = static_cast (n->data); if (p->flags & ASASL_MARKED_FOR_DELETION) { node_del (n, &sessions); destroy_session (p); node_free (n); } else p->flags |= ASASL_MARKED_FOR_DELETION; } } bool _modinit (module *m) { ConfTable::callback.ready.attach (on_config_ready); user_t::callback.sasl_input.attach (sasl_input); user_t::callback.add.attach (sasl_newuser); event_add ("sasl_delete_stale", delete_stale, NULL, 30); if (!cold_start) saslsvs.me = add_service (saslsvs.nick, saslsvs.user, saslsvs.host, saslsvs.real, saslserv); authservice_loaded++; return true; } void _moddeinit () { node_t *n, *tn; user_t::callback.sasl_input.detach (sasl_input); user_t::callback.add.detach (sasl_newuser); event_delete (delete_stale, NULL); if (saslsvs.me) { del_service (saslsvs.me); saslsvs.me = NULL; } authservice_loaded--; LIST_FOREACH_SAFE (n, tn, sessions.head) { destroy_session (static_cast (n->data)); node_del (n, &sessions); node_free (n); } ConfTable::callback.ready.detach (on_config_ready); }