angiosperm/extensions/resume.c

709 lines
19 KiB
C

#include "stdinc.h"
#include "channel.h"
#include "client.h"
#include "hash.h"
#include "hostmask.h"
#include "ircd.h"
#include "send.h"
#include "packet.h"
#include "s_conf.h"
#include "s_user.h"
#include "s_serv.h"
#include "msg.h"
#include "modules.h"
static const char resume_desc[] = "Provides RESUME and BRB";
#define BRB_TIMEOUT 300
static int _modinit(void);
static void _moddeinit(void);
static void m_resume(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_resumed(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_brb(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static bool resume_visible(struct Client *);
static void hook_cap_change(void *);
static void hook_umode_changed(void *);
static void hook_client_exit(void *);
static void hook_sendq_cleared(void *);
static unsigned int CLICAP_RESUME = 0;
static unsigned int CAP_RESUME = 0;
static rb_dictionary *resume_tree;
static rb_dlink_list brb_list;
static struct ev_entry *resume_check_ev;
static struct Message resume_msgtab = {
"RESUME", 0, 0, 0, 0,
{{m_resume, 2}, {m_resume, 2}, mg_ignore, mg_ignore, mg_ignore, {m_resume, 2}}
};
static struct Message resumed_msgtab = {
"RESUMED", 0, 0, 0, 0,
{mg_ignore, mg_ignore, {m_resumed, 2}, mg_ignore, mg_ignore, mg_ignore}
};
static struct Message brb_msgtab = {
"BRB", 0, 0, 0, 0,
{mg_unreg, {m_brb, 1}, mg_ignore, mg_ignore, mg_ignore, mg_ignore}
};
static mapi_clist_av1 resume_clist[] = {
&resume_msgtab,
&resumed_msgtab,
&brb_msgtab,
NULL
};
static mapi_hfn_list_av1 resume_hfnlist[] = {
{ "cap_change", hook_cap_change },
{ "umode_changed", hook_umode_changed },
{ "client_exit", hook_client_exit },
{ "sendq_cleared", hook_sendq_cleared },
{ NULL, NULL }
};
static struct ClientCapability resume_clicap = {
.visible = resume_visible
};
mapi_cap_list_av2 resume_cap_list[] = {
{ MAPI_CAP_CLIENT, "resume", &resume_clicap, &CLICAP_RESUME },
{ MAPI_CAP_SERVER, "RESUME", NULL, &CAP_RESUME },
{ 0, NULL, NULL, NULL }
};
DECLARE_MODULE_AV2(resume, _modinit, _moddeinit, resume_clist, NULL, resume_hfnlist, resume_cap_list, NULL, resume_desc);
enum brb_status
{
BRB_NO,
BRB_ARMED,
BRB_DONE
};
struct resume_session
{
struct Client *owner;
rb_dlink_node brb_node;
char *reason;
time_t brb_time;
enum brb_status brb;
unsigned char id[14];
unsigned char key[16];
};
static int cmp_resume(const void *a_, const void *b_)
{
const char *a = a_, *b = b_;
return memcmp(a, b, sizeof ((struct resume_session){0}).id);
}
static bool resume_visible(struct Client *client)
{
assert(MyConnect(client));
return IsSSL(client) && !IsOper(client);
}
static void resume_check(void *unused)
{
rb_dlink_node *n, *tmp;
time_t now = rb_current_time();
static char reason[BUFSIZE];
RB_DLINK_FOREACH_SAFE(n, tmp, brb_list.head)
{
struct resume_session *rs = n->data;
if (now < rs->brb_time + BRB_TIMEOUT)
break;
snprintf(reason, sizeof reason, "BRB: %s", rs->owner->user->away);
exit_client(NULL, rs->owner, rs->owner, reason);
}
}
static void sync_resumed_user(struct Client *target, bool brb)
{
rb_dlink_node *ptr;
buf_head_t tempq;
if (brb)
{
tempq = target->localClient->buf_sendq;
rb_linebuf_newbuf(&target->localClient->buf_sendq);
/* XXX assumes the welcome can be sent instantly */
user_welcome(target);
sendto_one(target, ":%s NOTE RESUME PLAYBACK :History playback starts here", me.name);
send_queued(target);
target->localClient->buf_sendq = tempq;
}
else
{
user_welcome(target);
}
RB_DLINK_FOREACH(ptr, target->user->channel.head)
{
struct membership *msptr = ptr->data;
struct Channel *chptr = msptr->chptr;
char mode[10], modeval[NICKLEN * 2 + 2], *mptr = mode;
*modeval = '\0';
sendto_one(target, ":%s!%s@%s JOIN %s", target->name, target->username, target->host, chptr->chname);
channel_member_names(chptr, target, 1);
if (is_chanop(msptr))
{
*mptr++ = 'o';
strcat(modeval, target->name);
strcat(modeval, " ");
}
if (is_voiced(msptr))
{
*mptr++ = 'v';
strcat(modeval, target->name);
}
if (*mode != '\0')
sendto_one(target, ":%s MODE %s +%s %s", me.name, chptr->chname, mode, modeval);
}
sendto_one(target, "RESUME SUCCESS %s", target->name);
if (!brb)
sendto_one(target, ":%s WARN RESUME HISTORY_LOST :No history was preserved for this session", me.name);
}
static void announce_resume(struct Client *client, const char *oldhost, const char *status)
{
rb_dlink_node *n;
sendto_server(client, NULL, CAP_RESUME | CAP_TS6, 0, ":%s RESUMED %s%s%s", use_id(client), client->host,
status ? " " : "",
status ? status : "");
sendto_server(client, NULL, CAP_EUID | CAP_TS6, CAP_RESUME, ":%s CHGHOST %s %s", use_id(client), use_id(client), client->host);
sendto_server(client, NULL, CAP_ENCAP | CAP_TS6, CAP_RESUME | CAP_EUID, ":%s ENCAP * CHGHOST %s %s", use_id(client), use_id(client), client->host);
sendto_common_channels_local_butone(client, CLICAP_RESUME, 0,
":%s!%s@%s RESUMED %s%s%s", client->name, client->username, oldhost, client->host,
status ? " " : "",
status ? status : "");
sendto_common_channels_local_butone(client, 0, CLICAP_RESUME,
":%s!%s@%s QUIT :Client reconnected", client->name, client->username, oldhost);
RB_DLINK_FOREACH(n, client->user->channel.head)
{
struct membership *msptr = n->data;
struct Channel *chptr = msptr->chptr;
char mode[10], modeval[NICKLEN * 2 + 2], *mptr = mode;
*modeval = '\0';
if (is_chanop(msptr))
{
*mptr++ = 'o';
strcat(modeval, client->name);
strcat(modeval, " ");
}
if (is_voiced(msptr))
{
*mptr++ = 'v';
strcat(modeval, client->name);
}
*mptr = '\0';
sendto_channel_local_with_capability_butone(client, ALL_MEMBERS, NOCAPS, CLICAP_EXTENDED_JOIN | CLICAP_RESUME, chptr,
":%s!%s@%s JOIN %s", client->name, client->username, client->host, chptr->chname);
sendto_channel_local_with_capability_butone(client, ALL_MEMBERS, CLICAP_EXTENDED_JOIN, CLICAP_RESUME, chptr,
":%s!%s@%s JOIN %s %s :%s", client->name, client->username, client->host, chptr->chname,
EmptyString(client->user->suser) ? "*" : client->user->suser, client->info);
if(*mode)
sendto_channel_local_with_capability_butone(client, ALL_MEMBERS, NOCAPS, CLICAP_RESUME, chptr,
":%s MODE %s +%s %s", client->servptr->name, chptr->chname, mode, modeval);
}
/* Resend away message to away-notify enabled clients. */
if (client->user->away)
sendto_common_channels_local_butone(client, CLICAP_AWAY_NOTIFY, CLICAP_RESUME,
":%s!%s@%s AWAY :%s", client->name, client->username, client->host, client->user->away);
}
static void enable_resume(struct Client *client_p)
{
struct resume_session *rs = rb_malloc(sizeof *rs);
static unsigned char token[sizeof rs->id + sizeof rs->key];
char *b64token;
rb_get_random(rs->id, sizeof rs->id);
rb_get_random(rs->key, sizeof rs->key);
rs->owner = client_p;
rs->brb = BRB_NO;
rs->reason = NULL;
client_p->localClient->resume = rs;
rb_dictionary_add(resume_tree, rs->id, rs);
memcpy(token, rs->id, sizeof rs->id);
memcpy(token + sizeof rs->id, rs->key, sizeof rs->key);
b64token = (char *)rb_base64_encode(token, sizeof token);
sendto_one(client_p, "RESUME TOKEN %s.%s", b64token, me.name);
rb_free(b64token);
}
static void disable_resume(struct Client *client_p)
{
struct resume_session *rs = client_p->localClient->resume;
if (rs == NULL)
return;
client_p->localClient->resume = NULL;
client_p->localClient->caps &= ~CLICAP_RESUME;
if (rs->brb == BRB_DONE)
rb_dlinkDelete(&rs->brb_node, &brb_list);
if (rs->reason != NULL)
rb_free(rs->reason);
rb_dictionary_delete(resume_tree, rs->id);
rb_free(rs);
}
static int memneq(const void *a_, const void *b_, size_t s)
{
volatile const unsigned char *a = a_, *b = b_;
volatile int r = 0;
for (size_t i = 0; i < s; i++)
{
r |= a[i] ^ b[i];
}
return r;
}
static bool invalidate_token(const char *tokstr)
{
int tl;
struct rb_dictionary_element *elem;
struct resume_session *rs;
char *dot = strchr(tokstr, '.');
unsigned char *token = rb_base64_decode((const unsigned char *)tokstr,
dot != NULL ? dot - tokstr : strlen(tokstr), &tl);
struct Client *owner;
if (tl != sizeof rs->id + sizeof rs->key)
{
rb_free(token);
return false;
}
elem = rb_dictionary_find(resume_tree, token);
if (!elem)
{
rb_free(token);
return false;
}
rs = elem->data;
if (memneq(token + sizeof rs->id, rs->key, sizeof rs->key))
{
rb_free(token);
return false;
}
rb_free(token);
owner = rs->owner;
disable_resume(owner);
enable_resume(owner);
return true;
}
static void resync_connids(struct Client *client)
{
rb_dlink_node *n;
RB_DLINK_FOREACH(n, client->localClient->connids.head)
{
uint32_t connid = RB_POINTER_TO_UINT(n->data);
del_from_cli_connid_hash(connid);
add_to_cli_connid_hash(client, connid);
}
}
static bool recheck_kline(struct Client *client)
{
struct ConfItem *aconf = find_kline(client);
if(aconf == NULL)
return false;
if(IsExemptKline(client))
{
sendto_realops_snomask(SNO_GENERAL, L_NETWIDE,
"KLINE over-ruled for %s, client is kline_exempt [%s@%s]",
get_client_name(client, HIDE_IP),
aconf->user, aconf->host);
return false;
}
sendto_realops_snomask(SNO_GENERAL, L_ALL,
"KLINE active for %s",
get_client_name(client, HIDE_IP));
notify_banned_client(client, aconf, K_LINED);
return true;
}
static void m_resume(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
{
/* find resume session */
struct rb_dictionary_element *elem;
struct resume_session *rs;
int tl;
char *dot = strchr(parv[1], '.');
assert(client_p == source_p);
if (!IsSSL(source_p))
{
sendto_one(source_p, ":%s FAIL RESUME INSECURE_SESSION :You must use TLS to resume sessions", me.name);
if (invalidate_token(parv[1]))
sendto_one_notice(source_p, "*** Your resume token was recognized, but has now been destroyed to protect against eavesdropping attacks.");
return;
}
if (!IsUnknown(source_p))
{
sendto_one(source_p, ":%s FAIL RESUME REGISTRATION_IS_COMPLETED :You have already registered", me.name);
return;
}
if (source_p->localClient->sasl_agent[0] != '\0' || source_p->localClient->sasl_complete)
{
sendto_one(source_p, ":%s FAIL RESUME CANNOT_RESUME :You must resume before starting SASL", me.name);
return;
}
if (dot != NULL && dot[1] != '\0' && irccmp(dot + 1, me.name))
{
sendto_one(source_p, ":%s FAIL RESUME WRONG_SERVER %s :This token must be redeemed on %s", me.name, dot + 1, dot + 1);
return;
}
unsigned char *token = rb_base64_decode((const unsigned char *)parv[1],
dot != NULL ? dot - parv[1] : strlen(parv[1]), &tl);
if (tl != sizeof rs->id + sizeof rs->key)
{
sendto_one(source_p, ":%s FAIL RESUME INVALID_TOKEN :Resume token unrecognized", me.name);
rb_free(token);
return;
}
elem = rb_dictionary_find(resume_tree, token);
if (!elem)
{
sendto_one(source_p, ":%s FAIL RESUME INVALID_TOKEN :Resume token unrecognized", me.name);
rb_free(token);
return;
}
rs = elem->data;
if (memneq(token + sizeof rs->id, rs->key, sizeof rs->key))
{
sendto_one(source_p, ":%s FAIL RESUME INVALID_TOKEN :Resume token unrecognized", me.name);
rb_free(token);
return;
}
rb_free(token);
struct Client *victim = rs->owner;
enum brb_status brb = rs->brb;
char *reason = rs->reason;
rs->reason = NULL;
disable_resume(victim);
if (IsOper(victim))
{
sendto_one(source_p, ":%s FAIL RESUME CANNOT_RESUME :Cowardly refusing to resume an oper", me.name);
rb_free(reason);
return;
}
rb_fde_t *tempF;
struct _ssl_ctl *tempssl;
struct ws_ctl *tempws;
buf_head_t tempq;
unsigned int tempcaps;
rb_dlink_list templ;
static char temphost[(HOSTLEN > HOSTIPLEN ? HOSTLEN : HOSTIPLEN) + 1];
tempF = source_p->localClient->F;
source_p->localClient->F = victim->localClient->F;
victim->localClient->F = tempF;
rb_setselect(victim->localClient->F, RB_SELECT_READ, read_packet, victim);
tempssl = source_p->localClient->ssl_ctl;
source_p->localClient->ssl_ctl = victim->localClient->ssl_ctl;
victim->localClient->ssl_ctl = tempssl;
tempws = source_p->localClient->ws_ctl;
source_p->localClient->ws_ctl = victim->localClient->ws_ctl;
victim->localClient->ws_ctl = tempws;
templ = source_p->localClient->connids;
source_p->localClient->connids = victim->localClient->connids;
victim->localClient->connids = templ;
resync_connids(source_p);
resync_connids(victim);
tempcaps = source_p->localClient->caps;
source_p->localClient->caps = victim->localClient->caps;
victim->localClient->caps = tempcaps;
strcpy(source_p->orighost, victim->orighost);
if (!IsIPSpoof(victim))
{
strcpy(victim->orighost, source_p->host);
}
strcpy(temphost, source_p->sockhost);
strcpy(source_p->sockhost, victim->sockhost);
strcpy(victim->sockhost, temphost);
strcpy(temphost, victim->host);
if (!IsIPSpoof(victim) && !IsDynSpoof(victim))
{
strcpy(victim->host, source_p->host);
strcpy(source_p->host, temphost);
}
if (irccmp(victim->host, victim->orighost))
SetDynSpoof(victim);
else
ClearDynSpoof(victim);
del_from_hostname_hash(source_p->orighost, victim);
add_to_hostname_hash(victim->orighost, victim);
rb_linebuf_donebuf(&victim->localClient->buf_recvq);
tempq = source_p->localClient->buf_recvq;
source_p->localClient->buf_recvq = victim->localClient->buf_recvq;
victim->localClient->buf_recvq = tempq;
if (source_p->localClient->resume != NULL)
{
victim->localClient->resume = source_p->localClient->resume;
victim->localClient->resume->owner = victim;
source_p->localClient->resume = NULL;
}
victim->localClient->sasl_complete = 0;
*victim->localClient->sasl_agent = '\0';
exit_client(source_p, source_p, source_p, "Connection resumed");
if (recheck_kline(victim))
{
rb_free(reason);
return;
}
if (brb != BRB_DONE)
rb_linebuf_donebuf(&victim->localClient->buf_sendq);
ClearFlush(victim);
sync_resumed_user(victim, brb == BRB_DONE);
if (IsAnyDead(victim))
{
rb_free(reason);
return;
}
if (brb == BRB_DONE)
{
if (reason == NULL)
{
free_away(victim);
sendto_server(victim, NULL, CAP_TS6, NOCAPS, ":%s AWAY", use_id(victim));
sendto_common_channels_local_butone(victim, CLICAP_AWAY_NOTIFY, NOCAPS, ":%s!%s@%s AWAY",
victim->name, victim->username, temphost);
}
else if (strncmp(victim->user->away, reason, AWAYLEN - 1))
{
rb_strlcpy(victim->user->away, reason, AWAYLEN);
sendto_server(victim, NULL, CAP_TS6, NOCAPS, ":%s AWAY :%s", use_id(victim), victim->user->away);
sendto_common_channels_local_butone(victim, CLICAP_AWAY_NOTIFY, NOCAPS,
":%s!%s@%s AWAY :%s", victim->name, victim->username, temphost, victim->user->away);
}
}
rb_free(reason);
const char *status = brb == BRB_DONE ? "ok" : NULL;
announce_resume(victim, temphost, status);
if (!IsIPSpoof(victim))
sendto_server(NULL, NULL, CAP_EUID | CAP_TS6, 0, ":%s ENCAP * REALHOST %s", use_id(victim), victim->orighost);
}
static void m_resumed(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
{
const char *status = NULL;
char oldhost[HOSTLEN + 1];
if (parc >= 3)
status = parv[2];
rb_strlcpy(oldhost, source_p->host, sizeof oldhost);
rb_strlcpy(source_p->host, parv[1], sizeof source_p->host);
announce_resume(source_p, oldhost, status);
}
static void do_brb(struct Client *client, const char *reason)
{
struct resume_session *rs = client->localClient->resume;
rs->brb = BRB_DONE;
sendto_one(client, "BRB %d", BRB_TIMEOUT);
send_queued(client);
SetFlush(client);
rb_close(client->localClient->F);
client->localClient->F = NULL;
client->localClient->lasttime = rb_current_time() + BRB_TIMEOUT;
rs->brb_time = rb_current_time();
rb_dlinkAddTail(rs, &rs->brb_node, &brb_list);
if (rs->reason != NULL)
{
rb_free(rs->reason);
rs->reason = NULL;
}
if (client->user->away == NULL)
allocate_away(client);
if (strncmp(client->user->away, reason, AWAYLEN - 1))
{
if (client->user->away[0] != '\0')
rs->reason = rb_strdup(client->user->away);
rb_strlcpy(client->user->away, reason, AWAYLEN);
sendto_server(client, NULL, CAP_TS6, NOCAPS, ":%s AWAY :%s", use_id(client), client->user->away);
sendto_common_channels_local_butone(client, CLICAP_AWAY_NOTIFY, NOCAPS,
":%s!%s@%s AWAY :%s", client->name, client->username, client->host, client->user->away);
}
}
static void m_brb(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char **parv)
{
struct resume_session *rs = source_p->localClient->resume;
if (rs == NULL)
{
sendto_one(source_p, ":%s FAIL BRB CANNOT_BRB :You do not have a resume token. CAP REQ resume first.", me.name);
return;
}
assert(rs->brb == BRB_NO);
rb_linebuf_donebuf(&source_p->localClient->buf_recvq);
rb_setselect(source_p->localClient->F, RB_SELECT_READ, NULL, NULL);
if (rb_linebuf_len(&source_p->localClient->buf_sendq) != 0)
{
rs->brb = BRB_ARMED;
rs->reason = rb_strdup(parv[1]);
}
else
{
do_brb(source_p, parv[1]);
}
}
static int _modinit(void)
{
rb_dlink_node *n;
resume_tree = rb_dictionary_create("resume", cmp_resume);
RB_DLINK_FOREACH(n, lclient_list.head)
{
struct Client *client = n->data;
struct resume_session *rs = client->localClient->resume;
bool is_resume = (client->localClient->caps & CLICAP_RESUME) != 0;
assert(!is_resume || rs);
if (!is_resume && rs != NULL)
{
client->localClient->resume = NULL;
rb_free(rs);
}
else if (rs != NULL)
{
if (IsOper(client))
disable_resume(client);
else
rb_dictionary_add(resume_tree, rs->id, rs);
}
}
resume_check_ev = rb_event_add("resume_check", resume_check, NULL, 10);
return 0;
}
static void _moddeinit(void)
{
rb_dictionary_destroy(resume_tree, NULL, NULL);
rb_event_delete(resume_check_ev);
}
static void hook_cap_change(void *data_)
{
hook_data_cap_change *data = data_;
if (data->del & CLICAP_RESUME)
disable_resume(data->client);
else if (data->add & CLICAP_RESUME)
enable_resume(data->client);
}
static void hook_umode_changed(void *data_)
{
hook_data_umode_changed *data = data_;
bool was_oper = !!(data->oldumodes & UMODE_OPER);
if (!MyClient(data->client))
return;
if (was_oper && !IsOper(data->client) && IsCapable(data->client, CLICAP_CAP_NOTIFY))
sendto_one(data->client, "CAP %s NEW :resume", data->client->name);
if (!was_oper && IsOper(data->client) && IsCapable(data->client, CLICAP_CAP_NOTIFY))
sendto_one(data->client, "CAP %s DEL :resume", data->client->name);
if (MyOper(data->client) && data->client->localClient->resume != NULL)
{
disable_resume(data->client);
sendto_one_notice(data->client, "You're too cool for resume");
}
}
static void hook_client_exit(void *data_)
{
hook_data_client_exit *data = data_;
if (!MyClient(data->target))
return;
if (data->target->localClient->resume)
disable_resume(data->target);
}
static void hook_sendq_cleared(void *data_)
{
hook_data_client *data = data_;
struct resume_session *rs = data->client->localClient->resume;
if (rs != NULL && rs->brb == BRB_ARMED)
{
do_brb(data->client, rs->reason);
}
}