pidgin/pidgin

Use the leaky bucket algorithm to rate limit irc messages.
release-2.x.y
2022-04-23, Gary Kramlich
c49dcf00bee6
Parents b6896c389190
Children 13cdb7956bdc
Use the leaky bucket algorithm to rate limit irc messages.

The default values were suggested by an operator of libera.

We don't rate limit the login process, nor parts and quits. However, if you
paste a bunch of text and then part a channel, you will be spammed with a
bunch of "no such nick/channel" error dialogs. I tried to work around this,
but the alternative just makes irc unresponsive until all the pasted messages
are sent. That said, other messages are still delayed while these errors
dialogs are slowly popping up.

Testing Done:
Lots

Bugs closed: PIDGIN-11089

Reviewed at https://reviews.imfreedom.org/r/524/
--- a/libpurple/protocols/irc/cmds.c Sat Apr 23 05:05:15 2022 -0500
+++ b/libpurple/protocols/irc/cmds.c Sat Apr 23 05:05:54 2022 -0500
@@ -390,7 +390,7 @@
buf = irc_format(irc, "vc:", "PART", args[0] ? args[0] : target, args[1]);
else
buf = irc_format(irc, "vc", "PART", args[0] ? args[0] : target);
- irc_send(irc, buf);
+ irc_priority_send(irc, buf);
g_free(buf);
return 0;
@@ -479,7 +479,7 @@
* decide we want custom quit messages.
*/
buf = irc_format(irc, "v:", "QUIT", (args && args[0]) ? args[0] : IRC_DEFAULT_QUIT);
- irc_send(irc, buf);
+ irc_priority_send(irc, buf);
g_free(buf);
irc->quitting = TRUE;
--- a/libpurple/protocols/irc/irc.c Sat Apr 23 05:05:15 2022 -0500
+++ b/libpurple/protocols/irc/irc.c Sat Apr 23 05:05:54 2022 -0500
@@ -29,6 +29,7 @@
#include "blist.h"
#include "conversation.h"
#include "debug.h"
+#include "glibcompat.h"
#include "notify.h"
#include "prpl.h"
#include "plugin.h"
@@ -95,9 +96,21 @@
ret = write(irc->fd, buf, len);
}
+ irc->send_time = time(NULL);
+
return ret;
}
+void irc_priority_send(struct irc_conn *irc, const char *buf)
+{
+ if(irc->sent_partial) {
+ g_queue_insert_after(irc->send_queue, irc->send_queue->head,
+ g_strdup(buf));
+ } else {
+ do_send(irc, buf, strlen(buf));
+ }
+}
+
static int irc_send_raw(PurpleConnection *gc, const char *buf, int len)
{
struct irc_conn *irc = (struct irc_conn*)gc->proto_data;
@@ -108,99 +121,125 @@
return len;
}
-static void
-irc_send_cb(gpointer data, gint source, PurpleInputCondition cond)
-{
- struct irc_conn *irc = data;
- int ret, writelen;
+static gboolean
+irc_send_handler_cb(gpointer data) {
+ struct irc_conn *irc = (struct irc_conn *)data;
+ gint available = 0;
+ gint interval = 0;
+
+ interval = purple_account_get_int(irc->account, "ratelimit-interval",
+ IRC_DEFAULT_COMMAND_INTERVAL);
- writelen = purple_circ_buffer_get_max_read(irc->outbuf);
-
- if (writelen == 0) {
- purple_input_remove(irc->writeh);
- irc->writeh = 0;
- return;
+ /* Check if we're enabled. */
+ if(interval < 1) {
+ available = G_MAXINT;
+ } else {
+ gint burst = purple_account_get_int(irc->account, "ratelimit-burst",
+ IRC_DEFAULT_COMMAND_MAX_BURST);
+ available = (time(NULL) - irc->send_time) / interval;
+ if(available > burst) {
+ available = burst;
+ }
}
- ret = do_send(irc, irc->outbuf->outptr, writelen);
+ while(available > 0) {
+ gchar *msg = NULL;
+ gpointer raw = NULL;
+ gint length = 0, ret = 0;
+
+ /* No message in the queue should be NULL, so a NULL value means the
+ * queue is empty.
+ */
+ raw = g_queue_pop_head(irc->send_queue);
+ if(raw == NULL) {
+ break;
+ }
+
+ msg = (gchar *)raw;
+ length = strlen(msg);
+
+ ret = do_send(irc, msg, length);
+ if(ret <= 0 && errno != EAGAIN) {
+ PurpleConnection *gc = purple_account_get_connection(irc->account);
+ gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
+ g_strerror(errno));
+
+ purple_connection_error_reason(gc,
+ PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
+ tmp);
+ g_free(tmp);
+
+ g_free(msg);
+
+ irc->send_handler = 0;
+
+ return G_SOURCE_REMOVE;
+ } else if(ret < length) {
+ gchar *partial = NULL;
- if (ret < 0 && errno == EAGAIN)
- return;
- else if (ret <= 0) {
- PurpleConnection *gc = purple_account_get_connection(irc->account);
- gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
- g_strerror(errno));
- purple_connection_error_reason (gc,
- PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
- g_free(tmp);
- return;
+ /* The preceeding conditional allows EAGAIN to fall through to
+ * here so that we can retransmit it. There shouldn't even be a
+ * case where ret < 0 and != EAGAIN, which is why we have the
+ * assert.
+ */
+ if(ret < 0) {
+ if(ret == EAGAIN) {
+ ret = 0;
+ } else {
+ g_assert_not_reached();
+ }
+ }
+
+ /* We need to move past the characters we already wrote and requeue
+ * the rest of the string. We know ret is less than length, so the
+ * starting address of msg plus ret can never get outside of the
+ * string, and likewise, length minus ret will always be < length
+ * because ret is less than length and if it was somehow negative,
+ * it has been reset to zero.
+ */
+ partial = g_strndup(msg + ret, length - ret);
+
+ /* requeue the item to the start of the queue */
+ g_queue_push_head(irc->send_queue, partial);
+ irc->sent_partial = TRUE;
+ } else {
+ /* We successfully sent a message so decrement the counter. */
+ available -= 1;
+ irc->sent_partial = FALSE;
+ }
+
+ /* Message was processed successfully or a partial message was
+ * allocated and requeued so we can free the one we popped off.
+ */
+ g_free(msg);
}
- purple_circ_buffer_mark_read(irc->outbuf, ret);
-
-#if 0
- /* We *could* try to write more if we wrote it all */
- if (ret == write_len) {
- irc_send_cb(data, source, cond);
- }
-#endif
+ return G_SOURCE_CONTINUE;
}
-int irc_send(struct irc_conn *irc, const char *buf)
+void irc_send(struct irc_conn *irc, const char *buf)
{
return irc_send_len(irc, buf, strlen(buf));
}
-int irc_send_len(struct irc_conn *irc, const char *buf, int buflen)
-{
- int ret;
- char *tosend = g_strdup(buf);
+void
+irc_send_len(struct irc_conn *irc, const char *buf, int buflen) {
+ char *tosend = g_strdup(buf);
purple_signal_emit(_irc_plugin, "irc-sending-text", purple_account_get_connection(irc->account), &tosend);
- if (tosend == NULL)
- return 0;
-
- if (!purple_strequal(tosend, buf)) {
- buflen = strlen(tosend);
+ if(tosend == NULL) {
+ return;
}
- if (purple_debug_is_verbose()) {
+ if(purple_debug_is_verbose()) {
char *clean = purple_utf8_salvage(tosend);
clean = g_strstrip(clean);
purple_debug_misc("irc", "<< %s\n", clean);
g_free(clean);
}
- /* If we're not buffering writes, try to send immediately */
- if (!irc->writeh)
- ret = do_send(irc, tosend, buflen);
- else {
- ret = -1;
- errno = EAGAIN;
- }
-
- /* purple_debug(PURPLE_DEBUG_MISC, "irc", "sent%s: %s",
- irc->gsc ? " (ssl)" : "", tosend); */
- if (ret <= 0 && errno != EAGAIN) {
- PurpleConnection *gc = purple_account_get_connection(irc->account);
- gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
- g_strerror(errno));
- purple_connection_error_reason (gc,
- PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
- g_free(tmp);
- } else if (ret < buflen) {
- if (ret < 0)
- ret = 0;
- if (!irc->writeh)
- irc->writeh = purple_input_add(
- irc->gsc ? irc->gsc->fd : irc->fd,
- PURPLE_INPUT_WRITE, irc_send_cb, irc);
- purple_circ_buffer_append(irc->outbuf, tosend + ret,
- buflen - ret);
- }
- g_free(tosend);
- return ret;
+ g_queue_push_tail(irc->send_queue, tosend);
}
/* XXX I don't like messing directly with these buddies */
@@ -357,7 +396,9 @@
gc->proto_data = irc = g_new0(struct irc_conn, 1);
irc->fd = -1;
irc->account = account;
- irc->outbuf = purple_circ_buffer_new(512);
+
+ irc->send_queue = g_queue_new();
+ irc->sent_partial = FALSE;
userparts = g_strsplit(username, "@", 2);
purple_connection_set_display_name(gc, userparts[0]);
@@ -406,6 +447,7 @@
const char *nickname, *identname, *realname;
struct irc_conn *irc = gc->proto_data;
const char *pass = purple_connection_get_password(gc);
+ gint interval, burst;
#ifdef HAVE_CYRUS_SASL
const gboolean use_sasl = purple_account_get_bool(irc->account, "sasl", FALSE);
#endif
@@ -417,7 +459,7 @@
else /* intended to fall through */
#endif
buf = irc_format(irc, "v:", "PASS", pass);
- if (irc_send(irc, buf) < 0) {
+ if (do_send(irc, buf, strlen(buf)) < 0) {
g_free(buf);
return FALSE;
}
@@ -450,7 +492,7 @@
strlen(realname) ? realname : nickname);
g_free(tmp);
g_free(server);
- if (irc_send(irc, buf) < 0) {
+ if (do_send(irc, buf, strlen(buf)) < 0) {
g_free(buf);
return FALSE;
}
@@ -459,7 +501,7 @@
buf = irc_format(irc, "vn", "NICK", nickname);
irc->reqnick = g_strdup(nickname);
irc->nickused = FALSE;
- if (irc_send(irc, buf) < 0) {
+ if (do_send(irc, buf, strlen(buf)) < 0) {
g_free(buf);
return FALSE;
}
@@ -467,6 +509,15 @@
irc->recv_time = time(NULL);
+ /* Give ourselves one full burst for startup commands. */
+ interval = purple_account_get_int(irc->account, "ratelimit-interval",
+ IRC_DEFAULT_COMMAND_INTERVAL);
+ burst = purple_account_get_int(irc->account, "ratelimit-burst",
+ IRC_DEFAULT_COMMAND_MAX_BURST);
+
+ irc->send_time = time(NULL) - (interval * burst);
+ irc->send_handler = g_timeout_add_seconds(1, irc_send_handler_cb, irc);
+
return TRUE;
}
@@ -541,10 +592,10 @@
g_string_free(irc->motd, TRUE);
g_free(irc->server);
- if (irc->writeh)
- purple_input_remove(irc->writeh);
-
- purple_circ_buffer_destroy(irc->outbuf);
+ g_queue_free_full(irc->send_queue, g_free);
+ if(irc->send_handler != 0) {
+ g_source_remove(irc->send_handler);
+ }
g_free(irc->mode_chars);
g_free(irc->reqnick);
@@ -1108,6 +1159,16 @@
prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
#endif
+ option = purple_account_option_int_new(_("Seconds between sending messages"),
+ "ratelimit-interval",
+ IRC_DEFAULT_COMMAND_INTERVAL);
+ prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+ option = purple_account_option_int_new(_("Maximum messages to send at once"),
+ "ratelimit-burst",
+ IRC_DEFAULT_COMMAND_MAX_BURST);
+ prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
_irc_plugin = plugin;
purple_prefs_remove("/plugins/prpl/irc/quitmsg");
--- a/libpurple/protocols/irc/irc.h Sat Apr 23 05:05:15 2022 -0500
+++ b/libpurple/protocols/irc/irc.h Sat Apr 23 05:05:54 2022 -0500
@@ -29,7 +29,6 @@
#include <sasl/sasl.h>
#endif
-#include "circbuffer.h"
#include "ft.h"
#include "roomlist.h"
#include "sslconn.h"
@@ -43,6 +42,13 @@
#define IRC_DEFAULT_QUIT "Leaving."
+/* By default set the command send interval to 2 seconds and allow bursting of
+ * 5 commands at once. This means, if we haven't sent a command in 10 seconds
+ * we can send 5 commands immediately with no penalty.
+ */
+#define IRC_DEFAULT_COMMAND_INTERVAL 2
+#define IRC_DEFAULT_COMMAND_MAX_BURST 5
+
#define IRC_BUFSIZE_INCREMENT 1024
#define IRC_MAX_BUFSIZE 16384
@@ -92,10 +98,12 @@
gboolean quitting;
- PurpleCircBuffer *outbuf;
- guint writeh;
+ time_t recv_time;
- time_t recv_time;
+ GQueue *send_queue;
+ time_t send_time;
+ guint send_handler;
+ gboolean sent_partial;
char *mode_chars;
char *reqnick;
@@ -119,8 +127,9 @@
typedef int (*IRCCmdCallback) (struct irc_conn *irc, const char *cmd, const char *target, const char **args);
-int irc_send(struct irc_conn *irc, const char *buf);
-int irc_send_len(struct irc_conn *irc, const char *buf, int len);
+void irc_send(struct irc_conn *irc, const char *buf);
+void irc_send_len(struct irc_conn *irc, const char *buf, int len);
+void irc_priority_send(struct irc_conn *irc, const char *buf);
gboolean irc_blist_timeout(struct irc_conn *irc);
gboolean irc_who_channel_timeout(struct irc_conn *irc);
void irc_buddy_query(struct irc_conn *irc);
--- a/libpurple/protocols/irc/msgs.c Sat Apr 23 05:05:15 2022 -0500
+++ b/libpurple/protocols/irc/msgs.c Sat Apr 23 05:05:54 2022 -0500
@@ -1256,7 +1256,7 @@
char *buf;
buf = irc_format(irc, "v:", "PONG", args[0]);
- irc_send(irc, buf);
+ irc_priority_send(irc, buf);
g_free(buf);
}