mirror of https://github.com/pissnet/pissircd.git
416 lines
12 KiB
C
416 lines
12 KiB
C
/*
|
|
* IRC - Internet Relay Chat, src/modules/labeled-response.c
|
|
* (C) 2019 Syzop & The UnrealIRCd Team
|
|
*
|
|
* See file AUTHORS in IRC package for additional names of
|
|
* the programmers.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 1, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
|
|
ModuleHeader MOD_HEADER
|
|
= {
|
|
"labeled-response",
|
|
"5.0",
|
|
"Labeled response CAP",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
/* Data structures */
|
|
typedef struct LabeledResponseContext LabeledResponseContext;
|
|
struct LabeledResponseContext {
|
|
Client *client; /**< The client who issued the original command with a label */
|
|
char label[256]; /**< The label attached to this command */
|
|
char batch[BATCHLEN+1]; /**< The generated batch id */
|
|
int responses; /**< Number of lines sent back to client */
|
|
int sent_remote; /**< Command has been sent to remote server */
|
|
char firstbuf[MAXLINELENGTH]; /**< First buffered response */
|
|
};
|
|
|
|
/* Forward declarations */
|
|
int lr_pre_command(Client *from, MessageTag *mtags, const char *buf);
|
|
int lr_post_command(Client *from, MessageTag *mtags, const char *buf);
|
|
int lr_close_connection(Client *client);
|
|
int lr_packet(Client *from, Client *to, Client *intended_to, char **msg, int *len);
|
|
void lr_new_message(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature);
|
|
void *_labeled_response_save_context(void);
|
|
void _labeled_response_set_context(void *ctx);
|
|
void _labeled_response_force_end(void);
|
|
|
|
/* Our special version of SupportBatch() assumes that remote servers always handle it */
|
|
#define SupportBatch(x) (MyConnect(x) ? HasCapability((x), "batch") : 1)
|
|
#define SupportLabel(x) (HasCapabilityFast((x), CAP_LABELED_RESPONSE))
|
|
|
|
/* Variables */
|
|
static LabeledResponseContext currentcmd;
|
|
static long CAP_LABELED_RESPONSE = 0L;
|
|
|
|
static char packet[MAXLINELENGTH*2];
|
|
|
|
int labeled_response_mtag_is_ok(Client *client, const char *name, const char *value);
|
|
|
|
MOD_TEST()
|
|
{
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
EfunctionAddPVoid(modinfo->handle, EFUNC_LABELED_RESPONSE_SAVE_CONTEXT, _labeled_response_save_context);
|
|
EfunctionAddVoid(modinfo->handle, EFUNC_LABELED_RESPONSE_SET_CONTEXT, _labeled_response_set_context);
|
|
EfunctionAddVoid(modinfo->handle, EFUNC_LABELED_RESPONSE_FORCE_END, _labeled_response_force_end);
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_INIT()
|
|
{
|
|
ClientCapabilityInfo cap;
|
|
ClientCapability *c;
|
|
MessageTagHandlerInfo mtag;
|
|
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
|
|
memset(¤tcmd, 0, sizeof(currentcmd));
|
|
|
|
memset(&cap, 0, sizeof(cap));
|
|
cap.name = "labeled-response";
|
|
c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_LABELED_RESPONSE);
|
|
|
|
memset(&mtag, 0, sizeof(mtag));
|
|
mtag.name = "label";
|
|
mtag.is_ok = labeled_response_mtag_is_ok;
|
|
mtag.clicap_handler = c;
|
|
MessageTagHandlerAdd(modinfo->handle, &mtag);
|
|
|
|
HookAdd(modinfo->handle, HOOKTYPE_PRE_COMMAND, -1000000000, lr_pre_command);
|
|
HookAdd(modinfo->handle, HOOKTYPE_POST_COMMAND, 1000000000, lr_post_command);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CLOSE_CONNECTION, 1000000000, lr_close_connection);
|
|
HookAdd(modinfo->handle, HOOKTYPE_PACKET, 1000000000, lr_packet);
|
|
HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, lr_new_message);
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD()
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_UNLOAD()
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
int lr_pre_command(Client *from, MessageTag *mtags, const char *buf)
|
|
{
|
|
memset(¤tcmd, 0, sizeof(currentcmd));
|
|
labeled_response_inhibit = labeled_response_inhibit_end = labeled_response_force = 0;
|
|
|
|
if (IsServer(from))
|
|
return 0;
|
|
|
|
for (; mtags; mtags = mtags->next)
|
|
{
|
|
if (!strcmp(mtags->name, "label") && mtags->value)
|
|
{
|
|
strlcpy(currentcmd.label, mtags->value, sizeof(currentcmd.label));
|
|
currentcmd.client = from;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
char *gen_start_batch(void)
|
|
{
|
|
static char buf[512];
|
|
|
|
generate_batch_id(currentcmd.batch);
|
|
|
|
if (MyConnect(currentcmd.client))
|
|
{
|
|
/* Local connection */
|
|
snprintf(buf, sizeof(buf), "@label=%s :%s BATCH +%s labeled-response",
|
|
currentcmd.label,
|
|
me.name,
|
|
currentcmd.batch);
|
|
} else {
|
|
/* Remote connection: requires intra-server BATCH syntax */
|
|
snprintf(buf, sizeof(buf), "@label=%s :%s BATCH %s +%s labeled-response",
|
|
currentcmd.label,
|
|
me.name,
|
|
currentcmd.client->name,
|
|
currentcmd.batch);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
int lr_post_command(Client *from, MessageTag *mtags, const char *buf)
|
|
{
|
|
/* ** IMPORTANT **
|
|
* Take care NOT to return here, use 'goto done' instead
|
|
* as some variables need to be cleared.
|
|
*/
|
|
|
|
/* We may have to send a response or end a BATCH here, if all of
|
|
* the following is true:
|
|
* 1. The client is still online (from is not NULL)
|
|
* 2. A "label" was attached
|
|
* 3. The client supports BATCH (or is remote)
|
|
* 4. The command has not been forwarded to a remote server
|
|
* (in which case they would be handling it, and not us)
|
|
* 5. Unless labeled_response_force is set, in which case
|
|
* we are assumed to have handled it anyway (necessary for
|
|
* commands like PRIVMSG, quite rare).
|
|
*/
|
|
if (from && currentcmd.client &&
|
|
!(currentcmd.sent_remote && !currentcmd.responses && !labeled_response_force))
|
|
{
|
|
Client *savedptr;
|
|
|
|
if (currentcmd.responses == 0)
|
|
{
|
|
MessageTag *m = safe_alloc(sizeof(MessageTag));
|
|
safe_strdup(m->name, "label");
|
|
safe_strdup(m->value, currentcmd.label);
|
|
memset(¤tcmd, 0, sizeof(currentcmd));
|
|
sendto_one(from, m, ":%s ACK", me.name);
|
|
free_message_tags(m);
|
|
goto done;
|
|
} else
|
|
if (currentcmd.responses == 1)
|
|
{
|
|
/* We have buffered this response earlier,
|
|
* now we will send it
|
|
*/
|
|
int more_tags = currentcmd.firstbuf[0] == '@';
|
|
currentcmd.client = NULL; /* prevent lr_packet from interfering */
|
|
snprintf(packet, sizeof(packet)-3,
|
|
"@label=%s%s%s",
|
|
currentcmd.label,
|
|
more_tags ? ";" : " ",
|
|
more_tags ? currentcmd.firstbuf+1 : currentcmd.firstbuf);
|
|
/* Format the IRC message correctly here, so we can take the
|
|
* quick path through sendbufto_one().
|
|
*/
|
|
strlcat(packet, "\r\n", sizeof(packet));
|
|
sendbufto_one(from, packet, strlen(packet));
|
|
goto done;
|
|
}
|
|
|
|
/* End the batch */
|
|
if (!labeled_response_inhibit_end)
|
|
{
|
|
savedptr = currentcmd.client;
|
|
currentcmd.client = NULL;
|
|
if (MyConnect(savedptr))
|
|
sendto_one(from, NULL, ":%s BATCH -%s", me.name, currentcmd.batch);
|
|
else
|
|
sendto_one(from, NULL, ":%s BATCH %s -%s", me.name, savedptr->name, currentcmd.batch);
|
|
}
|
|
}
|
|
done:
|
|
memset(¤tcmd, 0, sizeof(currentcmd));
|
|
labeled_response_inhibit = labeled_response_inhibit_end = labeled_response_force = 0;
|
|
return 0;
|
|
}
|
|
|
|
int lr_close_connection(Client *client)
|
|
{
|
|
/* Flush all data before closing connection */
|
|
lr_post_command(client, NULL, NULL);
|
|
return 0;
|
|
}
|
|
|
|
/** Helper function for lr_packet() to skip the message tags prefix,
|
|
* and possibly @batch as well.
|
|
*/
|
|
char *skip_tags(char *msg)
|
|
{
|
|
if (*msg != '@')
|
|
return msg;
|
|
if (!strncmp(msg, "@batch", 6))
|
|
{
|
|
char *p;
|
|
for (p = msg; *p; p++)
|
|
if ((*p == ';') || (*p == ' '))
|
|
return p;
|
|
}
|
|
return msg+1; /* just skip the '@' */
|
|
}
|
|
|
|
int lr_packet(Client *from, Client *to, Client *intended_to, char **msg, int *len)
|
|
{
|
|
if (currentcmd.client && !labeled_response_inhibit)
|
|
{
|
|
/* Labeled response is active */
|
|
if (currentcmd.client == intended_to)
|
|
{
|
|
/* Add the label */
|
|
if (currentcmd.responses == 0)
|
|
{
|
|
int n = *len;
|
|
if (n > sizeof(currentcmd.firstbuf))
|
|
n = sizeof(currentcmd.firstbuf);
|
|
strlcpy(currentcmd.firstbuf, *msg, n);
|
|
/* Don't send anything -- yet */
|
|
*msg = NULL;
|
|
*len = 0;
|
|
} else
|
|
if (currentcmd.responses == 1)
|
|
{
|
|
/* Start the batch now, normally this would be a sendto_one()
|
|
* but doing so is not possible since we are in the sending code :(
|
|
* The code below is almost unbearable to see, but the alternative
|
|
* is to use an intermediate buffer or pointer jugling, of which
|
|
* the former is slower than this implementation and with the latter
|
|
* it is easy to make a mistake and create an overflow issue.
|
|
* So guess I'll stick with this...
|
|
*/
|
|
char *batchstr = gen_start_batch();
|
|
int more_tags_one = currentcmd.firstbuf[0] == '@';
|
|
int more_tags_two = **msg == '@';
|
|
|
|
if (!strncmp(*msg, "@batch", 6))
|
|
{
|
|
/* Special case: current message (*msg) already contains a batch */
|
|
snprintf(packet, sizeof(packet),
|
|
"%s\r\n"
|
|
"@batch=%s%s%s\r\n"
|
|
"%s",
|
|
batchstr,
|
|
currentcmd.batch,
|
|
more_tags_one ? ";" : " ",
|
|
more_tags_one ? currentcmd.firstbuf+1 : currentcmd.firstbuf,
|
|
*msg);
|
|
} else
|
|
{
|
|
/* Regular case: current message (*msg) contains no batch yet, add one.. */
|
|
snprintf(packet, sizeof(packet),
|
|
"%s\r\n"
|
|
"@batch=%s%s%s\r\n"
|
|
"@batch=%s%s%s",
|
|
batchstr,
|
|
currentcmd.batch,
|
|
more_tags_one ? ";" : " ",
|
|
more_tags_one ? currentcmd.firstbuf+1 : currentcmd.firstbuf,
|
|
currentcmd.batch,
|
|
more_tags_two ? ";" : " ",
|
|
more_tags_two ? *msg+1 : *msg);
|
|
}
|
|
*msg = packet;
|
|
*len = strlen(*msg);
|
|
} else {
|
|
/* >2 responses.... the first 2 have already been sent */
|
|
if (!strncmp(*msg, "@batch", 6))
|
|
{
|
|
/* No buffer change needed, already contains a (now inner) batch */
|
|
} else {
|
|
int more_tags = **msg == '@';
|
|
snprintf(packet, sizeof(packet), "@batch=%s%s%s",
|
|
currentcmd.batch,
|
|
more_tags ? ";" : " ",
|
|
more_tags ? *msg+1 : *msg);
|
|
*msg = packet;
|
|
*len = strlen(*msg);
|
|
}
|
|
}
|
|
currentcmd.responses++;
|
|
}
|
|
else if (IsServer(to) || !MyUser(to))
|
|
{
|
|
currentcmd.sent_remote = 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void lr_new_message(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature)
|
|
{
|
|
/* Locally processed messages already have the label! */
|
|
if (currentcmd.client) return;
|
|
MessageTag *m;
|
|
|
|
m = find_mtag(recv_mtags, "label");
|
|
if (m)
|
|
{
|
|
m = duplicate_mtag(m);
|
|
AddListItem(m, *mtag_list);
|
|
}
|
|
}
|
|
|
|
/** This function verifies if the client sending the
|
|
* tag is permitted to do so and uses a permitted syntax.
|
|
*/
|
|
int labeled_response_mtag_is_ok(Client *client, const char *name, const char *value)
|
|
{
|
|
if (BadPtr(value))
|
|
return 0;
|
|
|
|
if (IsServer(client))
|
|
return 1;
|
|
|
|
/* Ignore the label if the client does not support both
|
|
* (draft/)labeled-response and batch. Yeah, batch too,
|
|
* it's too much hassle to support labeled-response without
|
|
* batch and the end result is quite broken too.
|
|
*/
|
|
if (MyUser(client) && (!SupportLabel(client) || !SupportBatch(client)))
|
|
return 0;
|
|
|
|
/* Do some basic sanity checking for non-servers */
|
|
if (strlen(value) <= 64)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** Save current context for later use in labeled-response.
|
|
* Currently used in /LIST. Is not planned for other places tbh.
|
|
*/
|
|
void *_labeled_response_save_context(void)
|
|
{
|
|
LabeledResponseContext *ctx = safe_alloc(sizeof(LabeledResponseContext));
|
|
memcpy(ctx, ¤tcmd, sizeof(LabeledResponseContext));
|
|
return (void *)ctx;
|
|
}
|
|
|
|
/** Set previously saved context 'ctx', or clear the context.
|
|
* @param ctx The context, or NULL to clear the context.
|
|
* @note The client from the previously saved context should be
|
|
* the same. Don't save one context when processing
|
|
* client A and then restore it when processing client B (duh).
|
|
*/
|
|
void _labeled_response_set_context(void *ctx)
|
|
{
|
|
if (ctx == NULL)
|
|
{
|
|
/* This means: clear the current context */
|
|
memset(¤tcmd, 0, sizeof(currentcmd));
|
|
} else {
|
|
/* Set the current context to the provided one */
|
|
memcpy(¤tcmd, ctx, sizeof(LabeledResponseContext));
|
|
}
|
|
}
|
|
|
|
/** Force an end of the labeled-response (only used in /LIST atm) */
|
|
void _labeled_response_force_end(void)
|
|
{
|
|
if (currentcmd.client)
|
|
lr_post_command(currentcmd.client, NULL, NULL);
|
|
}
|