From 7ffce705d09d464406be1a0b824009984cae30a3 Mon Sep 17 00:00:00 2001 From: Haw Loeung Date: Mon, 24 Aug 2026 08:17:51 +1000 Subject: [PATCH 1/2] Answer IRCv3 CAP commands to fix client connect hang in the proxy module irssi connecting to an irssi+proxy hangs indefinitely waiting for a CAP LS response. This change fixes this by answering to CAP LS with an empty list and NAK REQs so capability negotiation can finish cleanly. --- src/irc/proxy/listen.c | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/irc/proxy/listen.c b/src/irc/proxy/listen.c index 1fed639e..806cae96 100644 --- a/src/irc/proxy/listen.c +++ b/src/irc/proxy/listen.c @@ -164,6 +164,44 @@ static void grab_who(CLIENT_REC *client, const char *channel) g_string_free(arg, TRUE); } +static void handle_client_cap(CLIENT_REC *client, const char *args) +{ + char *subcmd, *cap_args; + const char *target_nick; + + subcmd = g_strdup(args); + cap_args = strchr(subcmd, ' '); + if (cap_args != NULL) { + *cap_args++ = '\0'; + while (*cap_args == ' ') + cap_args++; + } else { + cap_args = ""; + } + + target_nick = client->nick != NULL ? client->nick : "*"; + + if (g_ascii_strcasecmp(subcmd, "LS") == 0) { + proxy_outdata(client, ":%s CAP %s LS :\r\n", client->proxy_address, target_nick); + } else if (g_ascii_strcasecmp(subcmd, "REQ") == 0) { + const char *req = cap_args; + + while (*req == ' ' || *req == ':') + req++; + + if (*req != '\0') { + proxy_outdata(client, ":%s CAP %s NAK :%s\r\n", client->proxy_address, + target_nick, req); + } + } else if (g_ascii_strcasecmp(subcmd, "LIST") == 0) { + proxy_outdata(client, ":%s CAP %s LIST :\r\n", client->proxy_address, target_nick); + } else if (g_ascii_strcasecmp(subcmd, "END") == 0) { + /* CAP negotiation complete */ + } + + g_free(subcmd); +} + static void handle_client_connect_cmd(CLIENT_REC *client, const char *cmd, const char *args) { @@ -215,6 +253,8 @@ static void handle_client_connect_cmd(CLIENT_REC *client, client->nick = g_strdup(args); } else if (g_strcmp0(cmd, "USER") == 0) { client->user_sent = TRUE; + } else if (g_strcmp0(cmd, "CAP") == 0) { + handle_client_cap(client, args); } if (client->nick != NULL && client->user_sent) { @@ -241,6 +281,11 @@ static void handle_client_cmd(CLIENT_REC *client, char *cmd, char *args, return; } + if (g_strcmp0(cmd, "CAP") == 0) { + handle_client_cap(client, args); + return; + } + if (g_strcmp0(cmd, "QUIT") == 0) { remove_client(client); return; From 6e59eaa96cddb58b84c8907fb78b97b641e393eb Mon Sep 17 00:00:00 2001 From: Haw Loeung Date: Sun, 13 Sep 2026 05:25:37 +1000 Subject: [PATCH 2/2] Pass through message-tags, if supported --- src/irc/proxy/listen.c | 52 ++++++++++++++++++++++++++++++++++++++---- src/irc/proxy/proxy.h | 1 + 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/irc/proxy/listen.c b/src/irc/proxy/listen.c index 806cae96..4a53dec3 100644 --- a/src/irc/proxy/listen.c +++ b/src/irc/proxy/listen.c @@ -164,6 +164,14 @@ static void grab_who(CLIENT_REC *client, const char *channel) g_string_free(arg, TRUE); } +static gboolean server_has_message_tags(IRC_SERVER_REC *server) +{ + if (server == NULL || server->cap_supported == NULL) + return FALSE; + + return g_hash_table_lookup_extended(server->cap_supported, "message-tags", NULL, NULL); +} + static void handle_client_cap(CLIENT_REC *client, const char *args) { char *subcmd, *cap_args; @@ -182,19 +190,33 @@ static void handle_client_cap(CLIENT_REC *client, const char *args) target_nick = client->nick != NULL ? client->nick : "*"; if (g_ascii_strcasecmp(subcmd, "LS") == 0) { - proxy_outdata(client, ":%s CAP %s LS :\r\n", client->proxy_address, target_nick); + if (server_has_message_tags(client->server)) { + proxy_outdata(client, ":%s CAP %s LS :message-tags\r\n", client->proxy_address, target_nick); + } else { + proxy_outdata(client, ":%s CAP %s LS :\r\n", client->proxy_address, target_nick); + } } else if (g_ascii_strcasecmp(subcmd, "REQ") == 0) { const char *req = cap_args; while (*req == ' ' || *req == ':') req++; - if (*req != '\0') { + if (*req == '\0') { + g_free(subcmd); + return; + } + + if (g_ascii_strcasecmp(req, "message-tags") == 0 && server_has_message_tags(client->server)) { + client->cap_message_tags = TRUE; + proxy_outdata(client, ":%s CAP %s ACK :%s\r\n", client->proxy_address, + target_nick, req); + } else { proxy_outdata(client, ":%s CAP %s NAK :%s\r\n", client->proxy_address, target_nick, req); } } else if (g_ascii_strcasecmp(subcmd, "LIST") == 0) { - proxy_outdata(client, ":%s CAP %s LIST :\r\n", client->proxy_address, target_nick); + proxy_outdata(client, ":%s CAP %s LIST :%s\r\n", client->proxy_address, target_nick, + client->cap_message_tags ? "message-tags" : ""); } else if (g_ascii_strcasecmp(subcmd, "END") == 0) { /* CAP negotiation complete */ } @@ -604,7 +626,29 @@ static void sig_server_event(IRC_SERVER_REC *server, const char *line, } /* send the data to clients.. */ - proxy_outdata_all(server, "%s", next_line->str); + for (tmp = proxy_clients; tmp != NULL; tmp = tmp->next) { + CLIENT_REC *rec = tmp->data; + + if (!rec->connected || rec->server != server) + continue; + + if (rec->cap_message_tags) { + net_sendbuffer_send(rec->handle, next_line->str, next_line->len); + } else if (g_strcmp0(event, "event tagmsg") != 0) { + const char *untagged = next_line->str; + + if (*untagged == '@') { + untagged = strchr(untagged, ' '); + if (untagged != NULL) { + while (*untagged == ' ') + untagged++; + } else { + untagged = next_line->str; + } + } + net_sendbuffer_send(rec->handle, untagged, strlen(untagged)); + } + } g_free(event); } diff --git a/src/irc/proxy/proxy.h b/src/irc/proxy/proxy.h index 5b3d25f5..6c8baa1d 100644 --- a/src/irc/proxy/proxy.h +++ b/src/irc/proxy/proxy.h @@ -31,6 +31,7 @@ typedef struct { unsigned int connected:1; unsigned int want_ctcp:1; unsigned int multiplex:1; + unsigned int cap_message_tags:1; } CLIENT_REC; #endif