1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-06-27 21:16:38 +02:00

Add draft/multiline support with a default max-lines of 15 for known-users

and 7 for unknown-users (with max-bytes 5250 and 1500 respectively). This
allows pasting a short snippet of code, config file, text from a site, etc.

With multiline you have the guarantee that:
1) You will see the entire text with no delay between lines
2) You won't see another persons chat half-way through such a paste
3) For multiline supporting clients it is now clear that all the text
   belongs to each other, which can make selecting/copying it easier.
This basically means short snippets/pastes like that can be completely on
IRC again. No need for a pastebin for it. Though, you may still need such
a service if you are pasting more lines.

Regarding the implementation in UnrealIRCd:
* Clients without multiline get individual fallback lines (concat lines
  merged, blank lines skipped, as per spec). And we know that clients like
  weechat - which does support multiline - also shows all lines and not
  only a few plus snippet style "[.."]. That is another reason for only
  allowing 15 lines by default and not something much more. Otherwise all
  those clients would get a big wall of text, which just sucks.
* Spamfilter (also) runs on the full text of all lines together, so
  splitting a phrase across lines does not evade spamfilter.
* Fakelag: a client can send the BATCH start+PRIVMSG (or NOTICE)+BATCH end
  at full speed. We impose no fake lag there. Also, the multiline default
  max-lines and max-bytes are lower than the example class::recvq of 8000,
  so should be perfectly safe. If the entire BATCH is accepted then we
  will impose fake-lag afterwards, with a cap of 15 seconds maximum.
  If the BATCH is rejected, we impose half the fakelag plus 2sec.
* If the time between BATCH start and BATCH end is more than 15 seconds
  then the BATCH is rejected (set::multiline::batch-timeout).
* The BATCH is atomic (either you see it all, or you see none of it):
  * When the client sends it to server, it is buffered first.
  * Only after the batch close the server indicates if it is accepted
    or rejected. This has various reasons, two of them are: 1) The client
    is going to send everything in one go anyway and not wait for a
    response between each PRIVMSG, and 2) we can't do many checks in the
    buffering stage and skip those after, that would cause a TOCTOU
    problem (eg. a banned user still being able to speak).
  * If any line gets rejected due to spamfilter or other case
    (eg +c, +b ~text with block, etc etc), the entire batch is rejected
  * Locally we deliver all or nothing (as said)
  * S2S we buffer the batch as well, so if a server splits after having
    received 10 lines out of 15, then clients will not see anything.
* We send max-lines and max-bytes, this is the hard upper limit.
* A multiline can still be limited more tight if:
  * +f with 't' or 'm' restricts to fewer lines,
    eg +f [5t]:15, which means max 5 lines per 15 seconds,
    means the max accepted multiline is 5 for that channel.
  * +F works the same, except that default +F normal does not
    have a 't' at the moment and 'm' is very high (50) so
    practically not limited by default.
  * There will be a future +f flood subtype for some more control

TODO: we will send CAP NEW on unknown-users <-> known-users to
      indicate the new max-lines value if you transition security groups

TODO: chat history does not yet include multiline batches.
This commit is contained in:
Bram Matthys
2026-03-29 19:30:45 +02:00
parent 8bfc599697
commit b0dba4bede
18 changed files with 2205 additions and 81 deletions
+5
View File
@@ -791,6 +791,7 @@ extern MODVAR int dontspread;
extern MODVAR int labeled_response_inhibit;
extern MODVAR int labeled_response_inhibit_end;
extern MODVAR int labeled_response_force;
extern MODVAR int echo_message_inhibit;
/* Efuncs */
extern MODVAR void (*do_join)(Client *, int, const char **);
@@ -880,6 +881,7 @@ extern MODVAR int (*is_services_but_not_ulined)(Client *client);
extern MODVAR void (*parse_message_tags)(Client *cptr, char **str, MessageTag **mtag_list);
extern MODVAR const char *(*mtags_to_string)(MessageTag *m, Client *acptr);
extern MODVAR int (*can_send_to_channel)(Client *cptr, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtyp, ClientContext *clictx);
extern MODVAR int (*can_send_to_user)(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx, int flags);
extern MODVAR void (*broadcast_md_globalvar)(ModDataInfo *mdi, ModData *md);
extern MODVAR void (*broadcast_md_globalvar_cmd)(Client *except, Client *sender, const char *varname, const char *value);
extern MODVAR int (*tkl_ip_hash)(const char *ip);
@@ -948,6 +950,7 @@ extern MODVAR int (*utf8_get_block_number)(const char *name);
extern MODVAR void (*send_isupport)(Client *client);
extern MODVAR void (*isupport_check_for_changes)(void);
extern MODVAR int (*get_connections_from_ip)(Client *client);
extern MODVAR int (*get_floodprot_channel_max_lines)(Channel *channel);
/* /Efuncs */
/* TLS functions */
@@ -987,6 +990,7 @@ extern int add_silence_default_handler(Client *client, const char *mask, int sen
extern int del_silence_default_handler(Client *client, const char *mask);
extern int is_silenced_default_handler(Client *client, Client *acptr);
extern int get_connections_from_ip_default_handler(Client *client);
extern int get_floodprot_channel_max_lines_default_handler(Channel *channel);
extern void do_unreal_log_remote_deliver_default_handler(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized);
extern int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost, const char *autojoin_channels);
extern void webserver_send_response_default_handler(Client *client, int status, char *msg);
@@ -1532,6 +1536,7 @@ extern Tag *add_tag(Client *client, const char *name, int value);
extern void free_all_tags(Client *client);
extern void del_tag(Client *client, const char *name);
extern void bump_tag_serial(Client *client);
extern int valid_batch_reference_tag(const char *ref);
extern int valid_spamfilter_id(const char *s);
extern void download_complete_dontcare(OutgoingWebRequest *request, OutgoingWebResponse *response);
extern char *urlencode(const char *s, char *wbuf, int wlen);
+6
View File
@@ -534,6 +534,11 @@ typedef struct {
#define MTAG_HANDLER_FLAGS_NONE 0x0
/** This message-tag does not have a CAP REQ xx (eg: for "msgid") */
#define MTAG_HANDLER_FLAGS_NO_CAP_NEEDED 0x1
/** This tag should only appear on the first message of the
* multiline fallback (for clients that don't support multiline).
* Used by "msgid" and "+draft/reply".
*/
#define MTAG_HANDLER_FLAGS_FIRST_ONLY 0x2
/** Message Tag Handler */
struct MessageTagHandler {
@@ -2800,6 +2805,7 @@ enum EfunctionType {
EFUNC_SEND_ISUPPORT,
EFUNC_ISUPPORT_CHECK_FOR_CHANGES,
EFUNC_GET_CONNECTIONS_FROM_IP,
EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES,
};
/* Module flags */
+6
View File
@@ -1457,7 +1457,10 @@ typedef enum FloodOption {
FLD_CONVERSATIONS = 5, /**< max-concurrent-conversations */
FLD_LAG_PENALTY = 6, /**< lag-penalty / lag-penalty-bytes */
FLD_VHOST = 7, /**< vhost-flood */
FLD_MULTILINE = 8, /**< multiline max-lines / max-bytes */
} FloodOption;
#define MULTILINE_MAX_CONFIGURABLE_LINES 200 /**< Maximum configurable max-lines for multiline */
#define MULTILINE_MAX_CONFIGURABLE_BYTES 131072 /**< Maximum configurable max-bytes for multiline (128KB) */
#define MAXFLOODOPTIONS 10
typedef struct TrafficStats TrafficStats;
@@ -2662,6 +2665,9 @@ struct ConfigItem_badword {
#define SKIP_CTCP 0x8
#define CHECK_INVISIBLE 0x10
/* Flags for 'flags' in 'can_send_to_user' (and future: can_send_to_channel) */
#define CAN_SEND_SKIP_SPAMFILTER 0x1
typedef struct GeoIPResult GeoIPResult;
struct GeoIPResult {
char *country_code;
+12 -6
View File
@@ -111,10 +111,11 @@ void cmd_alias(ClientContext *clictx, Client *client, MessageTag *mtags, int par
{
const char *msg = parv[1];
const char *errmsg = NULL;
/* FIXME: when can_send_to_channel() gets 'int flags', pass
* alias->spamfilter ? 0 : CAN_SEND_SKIP_SPAMFILTER
*/
if (can_send_to_channel(client, channel, &msg, &errmsg, 0, clictx))
{
if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_CHANMSG, cmd, channel->name, 0, clictx, NULL))
return;
new_message(client, NULL, &mtags);
sendto_channel(channel, client, client->direction,
NULL, 0, SEND_ALL|SKIP_DEAF, mtags,
@@ -124,6 +125,8 @@ void cmd_alias(ClientContext *clictx, Client *client, MessageTag *mtags, int par
return;
}
}
if (IsDead(client))
return;
sendnumeric(client, ERR_CANNOTDOCOMMAND,
cmd, "You may not use this command at this time");
}
@@ -249,10 +252,11 @@ void cmd_alias(ClientContext *clictx, Client *client, MessageTag *mtags, int par
{
const char *msg = output;
const char *errmsg = NULL;
if (!can_send_to_channel(client, channel, &msg, &errmsg, 0, clictx))
/* FIXME: when can_send_to_channel() gets 'int flags', pass
* alias->spamfilter ? 0 : CAN_SEND_SKIP_SPAMFILTER
*/
if (can_send_to_channel(client, channel, &msg, &errmsg, 0, clictx))
{
if (alias->spamfilter && match_spamfilter(client, output, SPAMF_CHANMSG, cmd, channel->name, 0, clictx, NULL))
return;
new_message(client, NULL, &mtags);
sendto_channel(channel, client, client->direction,
NULL, 0, SEND_ALL|SKIP_DEAF, mtags,
@@ -262,7 +266,9 @@ void cmd_alias(ClientContext *clictx, Client *client, MessageTag *mtags, int par
return;
}
}
sendnumeric(client, ERR_CANNOTDOCOMMAND, cmd,
if (IsDead(client))
return;
sendnumeric(client, ERR_CANNOTDOCOMMAND, cmd,
"You may not use this command at this time");
}
else if (format->type == ALIAS_REAL)
+4
View File
@@ -118,6 +118,7 @@ int (*is_services_but_not_ulined)(Client *client);
void (*parse_message_tags)(Client *client, char **str, MessageTag **mtag_list);
const char *(*mtags_to_string)(MessageTag *m, Client *client);
int (*can_send_to_channel)(Client *client, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx);
int (*can_send_to_user)(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx, int flags);
void (*broadcast_md_globalvar)(ModDataInfo *mdi, ModData *md);
void (*broadcast_md_globalvar_cmd)(Client *except, Client *sender, const char *varname, const char *value);
int (*tkl_ip_hash)(const char *ip);
@@ -191,6 +192,7 @@ int (*utf8_get_block_number)(const char *name);
void (*send_isupport)(Client *client);
void (*isupport_check_for_changes)(void);
int (*get_connections_from_ip)(Client *client);
int (*get_floodprot_channel_max_lines)(Channel *channel);
Efunction *EfunctionAddMain(Module *module, EfunctionType eftype, int (*func)(), void (*vfunc)(), void *(*pvfunc)(), char *(*stringfunc)(), const char *(*conststringfunc)())
{
@@ -457,6 +459,7 @@ void efunctions_init(void)
efunc_init_function(EFUNC_TKL_TYPE_STRING, tkl_type_string, NULL, 0);
efunc_init_function(EFUNC_TKL_TYPE_CONFIG_STRING, tkl_type_config_string, NULL, 0);
efunc_init_function(EFUNC_CAN_SEND_TO_CHANNEL, can_send_to_channel, NULL, 0);
efunc_init_function(EFUNC_CAN_SEND_TO_USER, can_send_to_user, NULL, 0);
efunc_init_function(EFUNC_BROADCAST_MD_GLOBALVAR, broadcast_md_globalvar, NULL, 0);
efunc_init_function(EFUNC_BROADCAST_MD_GLOBALVAR_CMD, broadcast_md_globalvar_cmd, NULL, 0);
efunc_init_function(EFUNC_TKL_IP_HASH, tkl_ip_hash, NULL, 0);
@@ -533,4 +536,5 @@ void efunctions_init(void)
efunc_init_function(EFUNC_SEND_ISUPPORT, send_isupport, NULL, 0);
efunc_init_function(EFUNC_ISUPPORT_CHECK_FOR_CHANGES, isupport_check_for_changes, NULL, 0);
efunc_init_function(EFUNC_GET_CONNECTIONS_FROM_IP, get_connections_from_ip, get_connections_from_ip_default_handler, 0);
efunc_init_function(EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, get_floodprot_channel_max_lines, get_floodprot_channel_max_lines_default_handler, 0);
}
+94 -47
View File
@@ -430,6 +430,7 @@ int flood_option_is_old(const char *name)
"knock-flood",
"connect-flood",
"target-flood",
"multiline",
NULL
};
@@ -1829,6 +1830,7 @@ void config_setdefaultsettings(Configuration *i)
config_parse_flood_generic("4:120", i, "known-users", FLD_KNOCK); /* KNOCK protection: max 4 per 120s */
config_parse_flood_generic("10:15", i, "known-users", FLD_CONVERSATIONS); /* 10 users, new user every 15s */
config_parse_flood_generic("180:750", i, "known-users", FLD_LAG_PENALTY); /* 180 bytes / 750 msec */
config_parse_flood_generic("15:5250", i, "known-users", FLD_MULTILINE); /* max-lines=15, max-bytes=5250 */
/* - unknown-users */
config_parse_flood_generic("2:60", i, "unknown-users", FLD_NICK); /* NICK flood protection: max 2 per 60s */
config_parse_flood_generic("2:90", i, "unknown-users", FLD_JOIN); /* JOIN flood protection: max 2 per 90s */
@@ -1838,6 +1840,7 @@ void config_setdefaultsettings(Configuration *i)
config_parse_flood_generic("2:120", i, "unknown-users", FLD_KNOCK); /* KNOCK protection: max 2 per 120s */
config_parse_flood_generic("4:15", i, "unknown-users", FLD_CONVERSATIONS); /* 4 users, new user every 15s */
config_parse_flood_generic("90:1000", i, "unknown-users", FLD_LAG_PENALTY); /* 90 bytes / 1000 msec */
config_parse_flood_generic("7:1500", i, "unknown-users", FLD_MULTILINE); /* max-lines=7, max-bytes=1500 */
/* TLS options */
i->tls_options = safe_alloc(sizeof(TLSOptions));
@@ -8074,6 +8077,22 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce)
snprintf(buf, sizeof(buf), "%d:%ld", users, every);
config_parse_flood_generic(buf, &tempiConf, cepp->name, FLD_CONVERSATIONS);
}
else if (!strcmp(ceppp->name, "multiline"))
{
/* Same hack: store max-lines in limit, max-bytes in period */
char buf[64];
int max_lines = 0;
int max_bytes = 0;
for (cep4 = ceppp->items; cep4; cep4 = cep4->next)
{
if (!strcmp(cep4->name, "max-lines"))
max_lines = atoi(cep4->value);
else if (!strcmp(cep4->name, "max-bytes"))
max_bytes = config_checkval(cep4->value, CFG_SIZE);
}
snprintf(buf, sizeof(buf), "%d:%d", max_lines, max_bytes);
config_parse_flood_generic(buf, &tempiConf, cepp->name, FLD_MULTILINE);
}
}
if ((lag_penalty != -1) && (lag_penalty_bytes != -1))
{
@@ -9006,6 +9025,44 @@ int _test_set(ConfigFile *conf, ConfigEntry *ce)
}
continue; /* required here, due to checknull directly below */
}
else if (!strcmp(ceppp->name, "multiline"))
{
for (cep4 = ceppp->items; cep4; cep4 = cep4->next)
{
CheckNull(cep4);
if (!strcmp(cep4->name, "max-lines"))
{
int v = atoi(cep4->value);
if ((v < 2) || (v > MULTILINE_MAX_CONFIGURABLE_LINES))
{
config_error("%s:%i: set::anti-flood::multiline::max-lines: "
"value should be between 2 and %d",
cep4->file->filename, cep4->line_number,
MULTILINE_MAX_CONFIGURABLE_LINES);
errors++;
}
} else
if (!strcmp(cep4->name, "max-bytes"))
{
int v = config_checkval(cep4->value, CFG_SIZE);
if ((v < 256) || (v > MULTILINE_MAX_CONFIGURABLE_BYTES))
{
config_error("%s:%i: set::anti-flood::multiline::max-bytes: "
"value should be between 256 and %d",
cep4->file->filename, cep4->line_number,
MULTILINE_MAX_CONFIGURABLE_BYTES);
errors++;
}
} else
{
config_error_unknownopt(cep4->file->filename,
cep4->line_number, "set::anti-flood::multiline",
cep4->name);
errors++;
}
}
continue;
}
else if (!strcmp(ceppp->name, "maxchannelsperuser"))
{
CheckNull(ceppp);
@@ -10336,6 +10393,26 @@ int _test_offchans(ConfigFile *conf, ConfigEntry *ce)
return errors;
}
/** Convert alias type string to ALIAS_xxx constant.
* @returns ALIAS_SERVICES, ALIAS_STATS, etc., or 0 for unknown.
*/
static int alias_type_strtoval(const char *s)
{
if (!strcmp(s, "services"))
return ALIAS_SERVICES;
if (!strcmp(s, "stats"))
return ALIAS_STATS;
if (!strcmp(s, "normal"))
return ALIAS_NORMAL;
if (!strcmp(s, "command"))
return ALIAS_COMMAND;
if (!strcmp(s, "channel"))
return ALIAS_CHANNEL;
if (!strcmp(s, "real"))
return ALIAS_REAL;
return 0;
}
int _conf_alias(ConfigFile *conf, ConfigEntry *ce)
{
ConfigItem_alias *alias = NULL;
@@ -10374,16 +10451,7 @@ int _conf_alias(ConfigFile *conf, ConfigEntry *ce)
safe_strdup(format->parameters, cepp->value);
}
else if (!strcmp(cepp->name, "type")) {
if (!strcmp(cepp->value, "services"))
format->type = ALIAS_SERVICES;
else if (!strcmp(cepp->value, "stats"))
format->type = ALIAS_STATS;
else if (!strcmp(cepp->value, "normal"))
format->type = ALIAS_NORMAL;
else if (!strcmp(cepp->value, "channel"))
format->type = ALIAS_CHANNEL;
else if (!strcmp(cepp->value, "real"))
format->type = ALIAS_REAL;
format->type = alias_type_strtoval(cepp->value);
}
}
AddListItem(format, alias->format);
@@ -10394,16 +10462,7 @@ int _conf_alias(ConfigFile *conf, ConfigEntry *ce)
safe_strdup(alias->nick, cep->value);
}
else if (!strcmp(cep->name, "type")) {
if (!strcmp(cep->value, "services"))
alias->type = ALIAS_SERVICES;
else if (!strcmp(cep->value, "stats"))
alias->type = ALIAS_STATS;
else if (!strcmp(cep->value, "normal"))
alias->type = ALIAS_NORMAL;
else if (!strcmp(cep->value, "channel"))
alias->type = ALIAS_CHANNEL;
else if (!strcmp(cep->value, "command"))
alias->type = ALIAS_COMMAND;
alias->type = alias_type_strtoval(cep->value);
}
else if (!strcmp(cep->name, "spamfilter"))
alias->spamfilter = config_checkval(cep->value, CFG_YESNO);
@@ -10421,8 +10480,8 @@ int _conf_alias(ConfigFile *conf, ConfigEntry *ce)
int _test_alias(ConfigFile *conf, ConfigEntry *ce) {
int errors = 0;
ConfigEntry *cep, *cepp;
char has_type = 0, has_target = 0, has_format = 0;
char type = 0;
char has_type = 0, has_target = 0, has_format = 0, has_spamfilter = 0;
int type = 0;
if (!ce->items)
{
@@ -10492,17 +10551,7 @@ int _test_alias(ConfigFile *conf, ConfigEntry *ce) {
continue;
}
has_type = 1;
if (!strcmp(cepp->value, "services"))
;
else if (!strcmp(cepp->value, "stats"))
;
else if (!strcmp(cepp->value, "normal"))
;
else if (!strcmp(cepp->value, "channel"))
;
else if (!strcmp(cepp->value, "real"))
;
else
if (!alias_type_strtoval(cepp->value))
{
config_error("%s:%i: unknown alias type",
cepp->file->filename, cepp->line_number);
@@ -10565,24 +10614,16 @@ int _test_alias(ConfigFile *conf, ConfigEntry *ce) {
continue;
}
has_type = 1;
if (!strcmp(cep->value, "services"))
;
else if (!strcmp(cep->value, "stats"))
;
else if (!strcmp(cep->value, "normal"))
;
else if (!strcmp(cep->value, "channel"))
;
else if (!strcmp(cep->value, "command"))
type = 'c';
else {
type = alias_type_strtoval(cep->value);
if (!type)
{
config_error("%s:%i: unknown alias type",
cep->file->filename, cep->line_number);
errors++;
}
}
else if (!strcmp(cep->name, "spamfilter"))
;
has_spamfilter = 1;
else {
config_error_unknown(cep->file->filename, cep->line_number,
"alias", cep->name);
@@ -10595,18 +10636,24 @@ int _test_alias(ConfigFile *conf, ConfigEntry *ce) {
"alias::type");
errors++;
}
if (!has_format && type == 'c')
if (!has_format && (type == ALIAS_COMMAND))
{
config_error("%s:%d: alias::type is 'command' but no alias::format was specified",
ce->file->filename, ce->line_number);
errors++;
}
else if (has_format && type != 'c')
else if (has_format && (type != ALIAS_COMMAND))
{
config_error("%s:%d: alias::format specified when type is not 'command'",
ce->file->filename, ce->line_number);
errors++;
}
if (has_spamfilter && (type == ALIAS_CHANNEL))
{
config_warn("%s:%d: alias::spamfilter has no effect for channel aliases, "
"spamfilter is always checked for channel messages",
ce->file->filename, ce->line_number);
}
return errors;
}
+23 -1
View File
@@ -1469,6 +1469,11 @@ int get_connections_from_ip_default_handler(Client *client)
return 0;
}
int get_floodprot_channel_max_lines_default_handler(Channel *channel)
{
return INT_MAX;
}
int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass,
ConfigItem_class *clientclass, long modes, const char *snomask,
const char *vhost, const char *autojoin_channels)
@@ -2832,7 +2837,7 @@ const char *StripControlCodesEx(const char *text, char *output, size_t outputlen
/* strip color, bold, underline, and reverse codes from a string */
const char *StripControlCodes(const char *text)
{
static unsigned char new_str[4096];
static unsigned char new_str[8192];
return StripControlCodesEx(text, new_str, sizeof(new_str), 0);
}
@@ -2883,6 +2888,23 @@ int valid_spamfilter_id(const char *s)
return 1;
}
/** Check if a batch reference tag contains only valid characters.
* Per the batch spec: only ASCII letters, numbers, and hyphens.
*/
int valid_batch_reference_tag(const char *ref)
{
const char *p;
if (BadPtr(ref) || strlen(ref) > 48)
return 0;
for (p = ref; *p; p++)
{
if (!isalnum(*p) && *p != '-')
return 0;
}
return 1;
}
void download_complete_dontcare(OutgoingWebRequest *request, OutgoingWebResponse *response)
{
#ifdef DEBUGMODE
+1 -1
View File
@@ -69,7 +69,7 @@ MODULES= \
history_backend_null.so tkldb.so channeldb.so whowasdb.so \
restrict-commands.so rmtkl.so require-module.so \
account-notify.so \
message-tags.so batch.so \
message-tags.so batch.so multiline.so \
account-tag.so labeled-response.so link-security.so \
message-ids.so plaintext-policy.so server-time.so sts.so \
echo-message.so userip-tag.so userhost-tag.so geoip-tag.so \
+24 -2
View File
@@ -80,7 +80,24 @@ CMD_FUNC(cmd_batch)
Client *target;
char buf[512];
if (MyUser(client) || (parc < 3))
if (MyUser(client))
{
/* No module claimed this client-initiated batch */
if (parc >= 2 && parv[1][0] == '+')
{
if (!valid_batch_reference_tag(parv[1] + 1))
sendto_one(client, NULL, ":%s FAIL BATCH INVALID_REFTAG %s :Invalid batch reference tag", me.name, parv[1] + 1);
else
sendto_one(client, NULL, ":%s FAIL BATCH UNKNOWN_TYPE :Unknown batch type", me.name);
}
return;
}
if (parc < 3)
return;
if (parv[2][0] != '+' && parv[2][0] != '-')
return;
if (!valid_batch_reference_tag(parv[2] + 1))
return;
target = find_client(parv[1], NULL);
@@ -109,12 +126,17 @@ CMD_FUNC(cmd_batch)
/** This function verifies if the client sending
* 'batch' is permitted to do so and uses a permitted
* syntax.
* We simply allow batch ONLY from servers and with any syntax.
* We allow batch from servers (with any syntax) and from
* local users (so modules handling client-initiated batches,
* like draft/multiline, can see the tag on recv_mtags).
*/
int batch_mtag_is_ok(Client *client, const char *name, const char *value)
{
if (IsServer(client))
return 1;
if (MyUser(client) && !BadPtr(value) && valid_batch_reference_tag(value))
return 1;
return 0;
}
+21
View File
@@ -181,11 +181,13 @@ int parse_channel_mode_flood_failed(const char **error_out, ChannelFloodProtecti
int floodprot_server_quit(Client *client, MessageTag *mtags);
void inherit_settings(ChannelFloodProtection *from, ChannelFloodProtection *to);
void reapply_profiles(void);
int _get_floodprot_channel_max_lines(Channel *channel);
MOD_TEST()
{
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, floodprot_config_test_set_block);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, floodprot_config_test_antiflood_block);
EfunctionAdd(modinfo->handle, EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, _get_floodprot_channel_max_lines);
return MOD_SUCCESS;
}
@@ -1301,6 +1303,25 @@ ChannelFloodProtection *get_channel_flood_settings(Channel *channel, int what)
return NULL;
}
/** Return the maximum number of lines permitted by +f/+F 'm' and 't' limits.
* Returns INT_MAX if no relevant flood protection is set.
*/
int _get_floodprot_channel_max_lines(Channel *channel)
{
ChannelFloodProtection *fld;
int result = INT_MAX;
fld = get_channel_flood_settings(channel, CHFLD_MSG);
if (fld && fld->limit[CHFLD_MSG])
result = MIN(result, fld->limit[CHFLD_MSG]);
fld = get_channel_flood_settings(channel, CHFLD_TEXT);
if (fld && fld->limit[CHFLD_TEXT])
result = MIN(result, fld->limit[CHFLD_TEXT]);
return result;
}
int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx)
{
Membership *mb;
+4
View File
@@ -66,6 +66,8 @@ MOD_UNLOAD()
int em_chanmsg(Client *client, Channel *channel, int sendflags, const char *prefix, const char *target, MessageTag *mtags, const char *text, SendType sendtype)
{
if (echo_message_inhibit)
return 0;
if (MyUser(client) && HasCapabilityFast(client, CAP_ECHO_MESSAGE))
{
if (sendtype != SEND_TYPE_TAGMSG)
@@ -87,6 +89,8 @@ int em_chanmsg(Client *client, Channel *channel, int sendflags, const char *pref
int em_usermsg(Client *client, Client *to, MessageTag *mtags, const char *text, SendType sendtype)
{
if (echo_message_inhibit)
return 0;
if (MyUser(client) && HasCapabilityFast(client, CAP_ECHO_MESSAGE))
{
if (sendtype != SEND_TYPE_TAGMSG)
+1 -1
View File
@@ -46,7 +46,7 @@ MOD_INIT()
memset(&mtag, 0, sizeof(mtag));
mtag.name = "msgid";
mtag.is_ok = msgid_mtag_is_ok;
mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED | MTAG_HANDLER_FLAGS_FIRST_ONLY;
MessageTagHandlerAdd(modinfo->handle, &mtag);
HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_or_inherit_msgid);
+34 -20
View File
@@ -28,7 +28,7 @@ CMD_FUNC(cmd_notice);
CMD_FUNC(cmd_tagmsg);
void cmd_message(ClientContext *clictx, Client *client, MessageTag *recv_mtags, int parc, const char *parv[], SendType sendtype);
int _can_send_to_channel(Client *client, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx);
int can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx);
int _can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx, int flags);
/* Variables */
long CAP_MESSAGE_TAGS = 0; /**< Looked up at MOD_LOAD, may stay 0 if message-tags support is absent */
@@ -47,6 +47,7 @@ MOD_TEST()
MARK_AS_OFFICIAL_MODULE(modinfo);
EfunctionAddConstString(modinfo->handle, EFUNC_STRIPCOLORS, _StripColors);
EfunctionAdd(modinfo->handle, EFUNC_CAN_SEND_TO_CHANNEL, _can_send_to_channel);
EfunctionAdd(modinfo->handle, EFUNC_CAN_SEND_TO_USER, _can_send_to_user);
return MOD_SUCCESS;
}
@@ -83,7 +84,7 @@ MOD_UNLOAD()
* text: Pointer to a pointer to a text [in, out]
* cmd: Pointer to a pointer which contains the command to use [in, out]
*/
int can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx)
int _can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx, int flags)
{
int ret;
Hook *h;
@@ -114,12 +115,14 @@ int can_send_to_user(Client *client, Client *target, const char **msgtext, const
return 0;
}
// Possible FIXME: make match_spamfilter also use errmsg, or via a wrapper? or use same numeric?
if (MyUser(client) && (sendtype != SEND_TYPE_TAGMSG))
/* Spamfilter: run on original text, before hooks may transform it
* (e.g. +G censor). This ensures spamfilter catches the actual
* input even if a user mode would sanitize it for delivery.
*/
if (!(flags & CAN_SEND_SKIP_SPAMFILTER) && MyUser(client) && (sendtype != SEND_TYPE_TAGMSG))
{
int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_USERNOTICE : SPAMF_USERMSG);
const char *cmd = sendtype_to_cmd(sendtype);
if (match_spamfilter(client, *msgtext, spamtype, cmd, target->name, 0, clictx, NULL))
return 0;
}
@@ -291,12 +294,6 @@ void cmd_message(ClientContext *clictx, Client *client, MessageTag *recv_mtags,
targetstr = pfixchan;
}
if (IsVirus(client) && strcasecmp(channel->name, SPAMFILTER_VIRUSCHAN))
{
sendnotice(client, "You are only allowed to talk in '%s'", SPAMFILTER_VIRUSCHAN);
continue;
}
text = parv[2];
errmsg = NULL;
if (MyUser(client) && !IsULine(client))
@@ -323,14 +320,6 @@ void cmd_message(ClientContext *clictx, Client *client, MessageTag *recv_mtags,
if ((*parv[2] == '\001') && strncmp(&parv[2][1], "ACTION ", 7))
sendflags |= SKIP_CTCP;
if (MyUser(client) && (sendtype != SEND_TYPE_TAGMSG))
{
int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG);
if (match_spamfilter(client, text, spamtype, cmd, channel->name, 0, clictx, NULL))
return;
}
new_message(client, recv_mtags, &mtags);
RunHook(HOOKTYPE_PRE_CHANMSG, client, channel, &mtags, text, sendtype);
@@ -409,7 +398,7 @@ void cmd_message(ClientContext *clictx, Client *client, MessageTag *recv_mtags,
{
const char *errmsg = NULL;
text = parv[2];
if (!can_send_to_user(client, target, &text, &errmsg, sendtype, clictx))
if (!can_send_to_user(client, target, &text, &errmsg, sendtype, clictx, 0))
{
/* Message is discarded */
if (IsDead(client))
@@ -627,8 +616,14 @@ int ban_version(Client *client, const char *text)
* @returns Returns 1 if the user is allowed to send, otherwise 0.
* (note that this behavior was reversed in UnrealIRCd versions <5.x.
*/
/* FIXME: in a future major release, add 'int flags' parameter
* (matching can_send_to_user) so callers like aliases.c can pass
* CAN_SEND_SKIP_SPAMFILTER to opt out of the built-in spamfilter
* check. Can't add the parameter now without breaking the module API.
*/
int _can_send_to_channel(Client *client, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtype, ClientContext *clictx)
{
static char errbuf[256];
Membership *lp;
int member, i = 0;
Hook *h;
@@ -638,6 +633,25 @@ int _can_send_to_channel(Client *client, Channel *channel, const char **msgtext,
*errmsg = NULL;
if (IsVirus(client) && strcasecmp(channel->name, SPAMFILTER_VIRUSCHAN))
{
ircsnprintf(errbuf, sizeof(errbuf), "You are only allowed to talk in '%s'", SPAMFILTER_VIRUSCHAN);
*errmsg = errbuf;
return 0;
}
/* Spamfilter: run on original text, before hooks may transform it
* (e.g. +G censor). This ensures spamfilter catches the actual
* input even if a channel mode would sanitize it for delivery.
*/
if (sendtype != SEND_TYPE_TAGMSG)
{
int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG);
const char *cmd = sendtype_to_cmd(sendtype);
if (match_spamfilter(client, *msgtext, spamtype, cmd, channel->name, 0, clictx, NULL))
return 0;
}
member = IsMember(client, channel);
lp = find_membership_link(client->user->channel, channel);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -53,7 +53,7 @@ MOD_INIT()
memset(&mtag, 0, sizeof(mtag));
mtag.name = "+draft/reply";
mtag.is_ok = replytag_mtag_is_ok;
mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED | MTAG_HANDLER_FLAGS_FIRST_ONLY;
MessageTagHandlerAdd(modinfo->handle, &mtag);
HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_replytag);
+7
View File
@@ -753,6 +753,13 @@ static void stats_set_anti_flood(Client *client, FloodSettings *f)
f->name,
f->limit[i] == INT_MAX ? 0 : (int)f->limit[i]);
} else
if (i == FLD_MULTILINE)
{
sendtxtnumeric(client, "anti-flood::%s::multiline::max-lines: %d",
f->name, (int)f->limit[i]);
sendtxtnumeric(client, "anti-flood::%s::multiline::max-bytes: %d",
f->name, (int)f->period[i]);
} else
{
sendtxtnumeric(client, "anti-flood::%s::%s: %d per %s",
f->name, floodoption_names[i],
+1 -1
View File
@@ -5490,7 +5490,7 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
TKL *winner_tkl = NULL;
const char *str;
const char *str_deconfused = NULL;
char deconfused[512];
char deconfused[8192];
int ret = -1;
char *reason = NULL;
#ifdef SPAMFILTER_DETECTSLOW
+8 -1
View File
@@ -51,6 +51,13 @@ MODVAR int labeled_response_force = 0;
*/
MODVAR int labeled_response_inhibit_end = 0;
/* FIXME: in a future major release that allows module API changes,
* add sendflags to HOOKTYPE_USERMSG and use a SKIP_ECHO sendflags
* flag instead of this global. See HOOKTYPE_CHANMSG which already
* has sendflags.
*/
MODVAR int echo_message_inhibit = 0;
/** Set to 1 if an UTF8 incompatible nick character set is in use */
MODVAR int non_utf8_nick_chars_in_use = 0;
@@ -941,7 +948,7 @@ MODVAR const char *floodoption_names[] = {
"max-concurrent-conversations",
"lag-penalty",
"vhost-flood",
"max-channels-per-user",
"multiline",
NULL
};