mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-04 13:13:13 +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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user