# HG changeset patch
# Node ID 5b0ac716a1ab
# Parent  3820d0d16092
diff --git a/libpurple/protocols/ircv3/meson.build b/libpurple/protocols/ircv3/meson.build
--- a/libpurple/protocols/ircv3/meson.build
+++ b/libpurple/protocols/ircv3/meson.build
@@ -6,6 +6,7 @@
 	'purpleircv3parser.c',
 	'purpleircv3protocol.c',
 	'purpleircv3protocolim.c',
+	'purpleircv3sasl.c',
 ]
 
 IRCV3_HEADERS = [
@@ -16,6 +17,7 @@
 	'purpleircv3parser.h',
 	'purpleircv3protocol.h',
 	'purpleircv3protocolim.h',
+	'purpleircv3sasl.h',
 ]
 
 if DYNAMIC_IRCV3
@@ -27,7 +29,7 @@
 
 	ircv3_prpl = shared_library('ircv3', IRCV3_SOURCES + IRCV3_HEADERS,
 		c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-IRCv3"'],
-		dependencies : [sasl, libpurple_dep, glib, gio, ws2_32],
+		dependencies : [libpurple_dep, glib, gio, gsasl, ws2_32],
 		install : true,
 		install_dir : PURPLE_PLUGINDIR,
 		override_options : ['c_std=c99', 'warning_level=2'])
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.c b/libpurple/protocols/ircv3/purpleircv3capabilities.c
--- a/libpurple/protocols/ircv3/purpleircv3capabilities.c
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.c
@@ -20,6 +20,7 @@
 
 #include "purpleircv3connection.h"
 #include "purpleircv3core.h"
+#include "purpleircv3sasl.h"
 
 #define PURPLE_IRCV3_CAPABILITIES_VERSION "302"
 
@@ -105,8 +106,23 @@
 static void
 purple_ircv3_capabilities_default_ready_cb(PurpleIRCv3Capabilities *capabilities)
 {
+	PurpleAccount *account = NULL;
+	PurpleConnection *purple_connection = NULL;
 	gboolean found = FALSE;
 
+	purple_connection = PURPLE_CONNECTION(capabilities->connection);
+	account = purple_connection_get_account(purple_connection);
+
+	/* Don't request the sasl capability unless the user has selected the
+	 * require-password option.
+	 */
+	if(purple_account_get_require_password(account)) {
+		purple_ircv3_capabilities_lookup(capabilities, "sasl", &found);
+		if(found) {
+			purple_ircv3_sasl_request(capabilities);
+		}
+	}
+
 	/* cap-notify is implied when we use CAP LS 302, so this is really just to
 	 * make sure it's requested.
 	 */
diff --git a/libpurple/protocols/ircv3/purpleircv3connection.h b/libpurple/protocols/ircv3/purpleircv3connection.h
--- a/libpurple/protocols/ircv3/purpleircv3connection.h
+++ b/libpurple/protocols/ircv3/purpleircv3connection.h
@@ -25,6 +25,8 @@
 #include <gplugin.h>
 #include <gplugin-native.h>
 
+#include <sasl/sasl.h>
+
 #include <purple.h>
 
 G_BEGIN_DECLS
diff --git a/libpurple/protocols/ircv3/purpleircv3parser.c b/libpurple/protocols/ircv3/purpleircv3parser.c
--- a/libpurple/protocols/ircv3/purpleircv3parser.c
+++ b/libpurple/protocols/ircv3/purpleircv3parser.c
@@ -21,6 +21,7 @@
 #include "purpleircv3capabilities.h"
 #include "purpleircv3core.h"
 #include "purpleircv3messagehandlers.h"
+#include "purpleircv3sasl.h"
 
 struct _PurpleIRCv3Parser {
 	GObject parent;
@@ -398,6 +399,7 @@
 	purple_ircv3_parser_set_fallback_handler(parser,
 	                                         purple_ircv3_message_handler_fallback);
 
+	/* Core functionality. */
 	purple_ircv3_parser_add_handler(parser, "CAP",
 	                                purple_ircv3_capabilities_message_handler);
 	purple_ircv3_parser_add_handler(parser, "NOTICE",
@@ -406,4 +408,26 @@
 	                                purple_ircv3_message_handler_ping);
 	purple_ircv3_parser_add_handler(parser, "PRIVMSG",
 	                                purple_ircv3_message_handler_privmsg);
+
+	/* SASL stuff. */
+	purple_ircv3_parser_add_handler(parser, "900",
+	                                purple_ircv3_sasl_logged_in);
+	purple_ircv3_parser_add_handler(parser, "901",
+	                                purple_ircv3_sasl_logged_out);
+	purple_ircv3_parser_add_handler(parser, "902",
+	                                purple_ircv3_sasl_nick_locked);
+	purple_ircv3_parser_add_handler(parser, "903",
+	                                purple_ircv3_sasl_success);
+	purple_ircv3_parser_add_handler(parser, "904",
+	                                purple_ircv3_sasl_failed);
+	purple_ircv3_parser_add_handler(parser, "905",
+	                                purple_ircv3_sasl_message_too_long);
+	purple_ircv3_parser_add_handler(parser, "906",
+	                                purple_ircv3_sasl_aborted);
+	purple_ircv3_parser_add_handler(parser, "907",
+	                                purple_ircv3_sasl_already_authed);
+	purple_ircv3_parser_add_handler(parser, "908",
+	                                purple_ircv3_sasl_mechanisms);
+	purple_ircv3_parser_add_handler(parser, "AUTHENTICATE",
+	                                purple_ircv3_sasl_authenticate);
 }
diff --git a/libpurple/protocols/ircv3/purpleircv3sasl.h b/libpurple/protocols/ircv3/purpleircv3sasl.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3sasl.h
@@ -0,0 +1,45 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 2 of the License, 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef PURPLE_IRCV3_SASL_H
+#define PURPLE_IRCV3_SASL_H
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "purpleircv3capabilities.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL void purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities);
+
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_in(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_out(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_nick_locked(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_success(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_failed(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_message_too_long(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_aborted(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_already_authed(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_mechanisms(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_sasl_authenticate(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+
+G_END_DECLS
+
+#endif /* PURPLE_IRCV3_SASL_H */
diff --git a/libpurple/protocols/ircv3/purpleircv3sasl.c b/libpurple/protocols/ircv3/purpleircv3sasl.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3sasl.c
@@ -0,0 +1,595 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 2 of the License, 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <gsasl.h>
+
+#include "purpleircv3sasl.h"
+
+#include "purpleircv3capabilities.h"
+#include "purpleircv3connection.h"
+#include "purpleircv3core.h"
+
+#define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data")
+
+typedef struct {
+	PurpleConnection *connection;
+
+	Gsasl *ctx;
+	Gsasl_session *session;
+
+	char *current_mechanism;
+	GString *mechanisms;
+
+	GString *server_in_buffer;
+} PurpleIRCv3SASLData;
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static const char *
+purple_ircv3_sasl_get_username(PurpleConnection *connection) {
+	PurpleAccount *account = NULL;
+	const char *username = NULL;
+
+	account = purple_connection_get_account(connection);
+
+	username = purple_account_get_string(account, "sasl-login-name", "");
+	if(username != NULL && username[0] != '\0') {
+		return username;
+	}
+
+	return purple_connection_get_display_name(connection);
+}
+
+/******************************************************************************
+ * SASL Callbacks
+ *****************************************************************************/
+static int
+purple_ircv3_sasl_callback(G_GNUC_UNUSED Gsasl *ctx, Gsasl_session *session,
+                           Gsasl_property property)
+{
+	PurpleIRCv3SASLData *data = NULL;
+	int res = GSASL_OK;
+
+	data = gsasl_session_hook_get(session);
+
+	switch(property) {
+		case GSASL_AUTHID:
+			res = gsasl_property_set(session, GSASL_AUTHID,
+			                         purple_ircv3_sasl_get_username(data->connection));
+			break;
+		case GSASL_AUTHZID:
+			/* AUTHZID is typically set to empty string because it's the user
+			 * logging in on their own behalf. Since IRCv3 doesn't really let
+			 * and admin login as a normal user, we always set it to empty
+			 * string.
+			 *
+			 * See https://www.gnu.org/software/gsasl/manual/gsasl.html#PLAIN
+			 * for further explanation.
+			 */
+			res = gsasl_property_set(session, GSASL_AUTHZID, "");
+			break;
+		case GSASL_PASSWORD:
+			res = gsasl_property_set(session, GSASL_PASSWORD,
+			                         purple_connection_get_password(data->connection));
+			break;
+		default:
+			g_warning("Unknown property %d", property);
+			break;
+	}
+
+	return res;
+}
+
+/******************************************************************************
+ * SASL Helpers
+ *****************************************************************************/
+static void
+purple_ircv3_sasl_connection_error(PurpleConnection *connection, int res,
+                                   int err, const char *msg)
+{
+	GError *error = NULL;
+
+	error = g_error_new(PURPLE_CONNECTION_ERROR, err, "%s: %s", msg,
+	                    gsasl_strerror(res));
+
+	purple_connection_take_error(connection, error);
+}
+
+static PurpleIRCv3SASLData *
+purple_ircv3_sasl_data_new(PurpleConnection *connection, Gsasl *ctx,
+                           const char *mechanisms)
+{
+	PurpleIRCv3SASLData *data = NULL;
+	GStrv parts = NULL;
+
+	data = g_new0(PurpleIRCv3SASLData, 1);
+
+	data->connection = g_object_ref(connection);
+	data->ctx = ctx;
+	gsasl_callback_set(data->ctx, purple_ircv3_sasl_callback);
+
+	/* Create a GString for the mechanisms have a leading and trailing ` `.
+	 * This is so we can easily remove attempted mechanism by removing
+	 * ` <attempted_mechanism> ' from the string which will make sure we're
+	 * always removing the proper mechanism. This is necessary because some
+	 * mechanisms have the same prefix, and others have the same suffix, which
+	 * could lead to incorrect removals.
+	 *
+	 * For example, if the list contains `EAP-AES128-PLUS EAP-AES128` and we
+	 * try `EAP-AES128` first, that means would remove `EAP-AES128` from the
+	 * list which would first find `EAP-AES128-PLUS` and replace it with an
+	 * empty string, which would leave the list as `-PLUS EAP-AES128` which is
+	 * obviously wrong. Instead the trailing space means our replace can be
+	 * `EAP-AES128 ` instead, which will get the proper value and update the
+	 * list to just contain `EAP-AES128-PLUS `.
+	 *
+	 * For a list of mechanisms see
+	 * https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml
+	 *
+	 */
+	data->mechanisms = g_string_new("");
+
+	parts = g_strsplit(mechanisms, ",", -1);
+	for(int i = 0; parts[i] != NULL; i++) {
+		g_string_append_printf(data->mechanisms, " %s ", parts[i]);
+	}
+	g_strfreev(parts);
+
+	return data;
+}
+
+static void
+purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) {
+	g_clear_object(&data->connection);
+	g_clear_pointer(&data->session, gsasl_finish);
+	g_clear_pointer(&data->ctx, gsasl_done);
+	g_clear_pointer(&data->current_mechanism, g_free);
+
+	g_string_free(data->mechanisms, TRUE);
+	data->mechanisms = NULL;
+
+	g_free(data);
+}
+
+static void
+purple_ircv3_sasl_attempt_mechanism(PurpleIRCv3Connection *connection,
+                                    PurpleIRCv3SASLData *data,
+                                    const char *next_mechanism)
+{
+	int res = GSASL_OK;
+
+	g_free(data->current_mechanism);
+	data->current_mechanism = g_strdup(next_mechanism);
+
+	res = gsasl_client_start(data->ctx, next_mechanism, &data->session);
+	if(res != GSASL_OK) {
+		purple_ircv3_sasl_connection_error(PURPLE_CONNECTION(connection),
+		                                   res,
+		                                   PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+		                                   _("Failed to setup SASL client"));
+		return;
+	}
+	gsasl_session_hook_set(data->session, data);
+
+	purple_ircv3_connection_writef(connection, "AUTHENTICATE %s",
+	                               next_mechanism);
+}
+
+static void
+purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) {
+	PurpleIRCv3SASLData *data = NULL;
+	PurpleAccount *account = NULL;
+	const char *next_mechanism = NULL;
+	gboolean allow_plain = TRUE;
+	gboolean good_mechanism = FALSE;
+	int res = 0;
+
+	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
+
+	/* If this is not our first attempt, remove the previous mechanism from the
+	 * list of mechanisms to try.
+	 */
+	if(data->current_mechanism != NULL) {
+		char *to_remove = g_strdup_printf(" %s ", data->current_mechanism);
+
+		g_message("SASL '%s' mechanism failed", data->current_mechanism);
+
+		g_string_replace(data->mechanisms, to_remove, "", 0);
+		g_free(to_remove);
+	}
+
+	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
+	if(!purple_account_get_bool(account, "use-tls", FALSE)) {
+		if(!purple_account_get_bool(account, "plain-sasl-in-clear", FALSE)) {
+			allow_plain = FALSE;
+		}
+	}
+
+	while(!good_mechanism) {
+		good_mechanism = TRUE;
+
+		next_mechanism = gsasl_client_suggest_mechanism(data->ctx,
+		                                                data->mechanisms->str);
+
+		if(next_mechanism == NULL) {
+			GError *error = g_error_new(PURPLE_CONNECTION_ERROR,
+			                            PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+			                            _("No valid SASL mechanisms found"));
+
+			purple_connection_take_error(PURPLE_CONNECTION(connection), error);
+
+			return;
+		}
+
+		if(purple_strequal(next_mechanism, "PLAIN") && !allow_plain) {
+			g_message("skipping SASL 'PLAIN' as it's not allowed without tls");
+
+			good_mechanism = FALSE;
+
+			g_string_replace(data->mechanisms, " PLAIN ", "", 0);
+		}
+	}
+
+	g_message("trying SASL '%s' mechanism", next_mechanism);
+
+	res = gsasl_client_start(data->ctx, next_mechanism, &data->session);
+	if(res != GSASL_OK) {
+		g_warning("Unexpected SASL response: %s", gsasl_strerror(res));
+
+		return;
+	}
+
+	purple_ircv3_sasl_attempt_mechanism(connection, data, next_mechanism);
+}
+
+static void
+purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) {
+	PurpleIRCv3Connection *connection = NULL;
+	PurpleIRCv3SASLData *data = NULL;
+	PurpleConnection *purple_connection = NULL;
+	Gsasl *ctx = NULL;
+	const char *advertised = NULL;
+	gint res;
+
+	connection = purple_ircv3_capabilities_get_connection(caps);
+	purple_connection = PURPLE_CONNECTION(connection);
+
+	res = gsasl_init(&ctx);
+	if(res != GSASL_OK) {
+		purple_ircv3_sasl_connection_error(purple_connection, res,
+		                                   PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+		                                   _("Failed to initialize SASL"));
+
+		return;
+	}
+
+	/* At this point we are ready to start our sasl negotiation, so add a wait
+	 * counter to the capabilities and start the negotiations!
+	 */
+	purple_ircv3_capabilities_add_wait(caps);
+
+	/* Grab the mechanisms that the server advertised and save them on the
+	 * connection. */
+	advertised = purple_ircv3_capabilities_lookup(caps, "sasl", NULL);
+
+	/* Create our SASLData object, add it to the connection. */
+	data = purple_ircv3_sasl_data_new(purple_connection, ctx, advertised);
+	g_object_set_data_full(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY,
+	                       data, (GDestroyNotify)purple_ircv3_sasl_data_free);
+
+	/* Make it go! */
+	purple_ircv3_sasl_attempt(connection);
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_ircv3_sasl_ack_cb(PurpleIRCv3Capabilities *caps, const char *capability,
+                         G_GNUC_UNUSED gpointer data)
+{
+	if(!purple_strequal(capability, "sasl")) {
+		return;
+	}
+
+	purple_ircv3_sasl_start(caps);
+}
+
+/******************************************************************************
+ * Internal API
+ *****************************************************************************/
+void
+purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities) {
+	purple_ircv3_capabilities_request(capabilities, "sasl");
+
+	g_signal_connect(capabilities, "ack",
+	                 G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL);
+}
+
+gboolean
+purple_ircv3_sasl_logged_in(G_GNUC_UNUSED GHashTable *tags,
+                            G_GNUC_UNUSED const char *source,
+                            G_GNUC_UNUSED const char *command,
+                            G_GNUC_UNUSED guint n_params,
+                            G_GNUC_UNUSED GStrv params,
+                            G_GNUC_UNUSED GError **error,
+                            G_GNUC_UNUSED gpointer data)
+{
+	/* At this point, we have the users authenticated username, we _may_ want
+	 * to update the account's ID to this, but we'll need more testing to
+	 * verify that.
+	 * -- GK 2023-01-12
+	 */
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_logged_out(G_GNUC_UNUSED GHashTable *tags,
+                             G_GNUC_UNUSED const char *source,
+                             G_GNUC_UNUSED const char *command,
+                             G_GNUC_UNUSED guint n_params,
+                             G_GNUC_UNUSED GStrv params,
+                             G_GNUC_UNUSED GError **error,
+                             G_GNUC_UNUSED gpointer data)
+{
+	/* Not sure how to trigger this or what we should do in this case to be
+	 * honest, so just note it for now.
+	 * -- GK 2023-01-12
+	 */
+	g_warning("Sever sent SASL logged out");
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_nick_locked(G_GNUC_UNUSED GHashTable *tags,
+                              G_GNUC_UNUSED const char *source,
+                              G_GNUC_UNUSED const char *command,
+                              G_GNUC_UNUSED guint n_params,
+                              G_GNUC_UNUSED GStrv params,
+                              GError **error,
+                              G_GNUC_UNUSED gpointer data)
+{
+	char *message = g_strjoinv(" ", params);
+
+	g_set_error(error, PURPLE_CONNECTION_ERROR,
+	            PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+	            _("Nick name is locked: %s"), message);
+
+	g_free(message);
+
+	return FALSE;
+}
+
+
+gboolean
+purple_ircv3_sasl_success(G_GNUC_UNUSED GHashTable *tags,
+                          G_GNUC_UNUSED const char *source,
+                          G_GNUC_UNUSED const char *command,
+                          G_GNUC_UNUSED guint n_params,
+                          G_GNUC_UNUSED GStrv params,
+                          G_GNUC_UNUSED GError **error,
+                          gpointer data)
+{
+	PurpleIRCv3Capabilities *capabilities = NULL;
+	PurpleIRCv3Connection *connection = data;
+	PurpleIRCv3SASLData *sasl_data = NULL;
+
+	capabilities = purple_ircv3_connection_get_capabilities(connection);
+
+	purple_ircv3_capabilities_remove_wait(capabilities);
+
+	sasl_data = g_object_get_data(G_OBJECT(connection),
+	                              PURPLE_IRCV3_SASL_DATA_KEY);
+
+	g_message("successfully authenticated with SASL '%s' mechanism.",
+	          sasl_data->current_mechanism);
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_failed(G_GNUC_UNUSED GHashTable *tags,
+                         G_GNUC_UNUSED const char *source,
+                         G_GNUC_UNUSED const char *command,
+                         G_GNUC_UNUSED guint n_params,
+                         G_GNUC_UNUSED GStrv params,
+                         G_GNUC_UNUSED GError **error,
+                         gpointer data)
+{
+	PurpleIRCv3Connection *connection = data;
+
+	purple_ircv3_sasl_attempt(connection);
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED GHashTable *tags,
+                                   G_GNUC_UNUSED const char *source,
+                                   G_GNUC_UNUSED const char *command,
+                                   G_GNUC_UNUSED guint n_params,
+                                   G_GNUC_UNUSED GStrv params,
+                                   G_GNUC_UNUSED GError **error,
+                                   G_GNUC_UNUSED gpointer data)
+{
+	g_message("sasl message too long received");
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_aborted(G_GNUC_UNUSED GHashTable *tags,
+                          G_GNUC_UNUSED const char *source,
+                          G_GNUC_UNUSED const char *command,
+                          G_GNUC_UNUSED guint n_params,
+                          G_GNUC_UNUSED GStrv params,
+                          G_GNUC_UNUSED GError **error,
+                          G_GNUC_UNUSED gpointer data)
+{
+	/* This is supposed to get sent, when the client sends `AUTHENTICATE *`,
+	 * but we don't do this, so I guess we'll note it for now...?
+	 * --GK 2023-01-12
+	 */
+	g_warning("The server claims we aborted SASL authentication.");
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_already_authed(G_GNUC_UNUSED GHashTable *tags,
+                                 G_GNUC_UNUSED const char *source,
+                                 G_GNUC_UNUSED const char *command,
+                                 G_GNUC_UNUSED guint n_params,
+                                 G_GNUC_UNUSED GStrv params,
+                                 G_GNUC_UNUSED GError **error,
+                                 G_GNUC_UNUSED gpointer data)
+{
+	/* Similar to aborted above, we don't allow this, so just note that it
+	 * happened.
+	 * -- GK 2023-01-12
+	 */
+	g_warning("Server claims we tried to SASL authenticate again.");
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_mechanisms(G_GNUC_UNUSED GHashTable *tags,
+                             G_GNUC_UNUSED const char *source,
+                             G_GNUC_UNUSED const char *command,
+                             guint n_params,
+                             GStrv params,
+                             G_GNUC_UNUSED GError **error,
+                             G_GNUC_UNUSED gpointer data)
+{
+	/* We need to find a server that sends this message. The specification says
+	 * it _may_ be sent when the client sends AUTHENTICATE with an unknown
+	 * mechanism, but ergo doesn't.
+	 *
+	 * We can't just blindly accept the new list either, as depending on how
+	 * the server implements it, we'll need to remove mechanisms we've already
+	 * tried in the event the server just dumps the entire list. As we're not
+	 * currently tracking which mechanisms we've tried, this will have to be
+	 * addressed as well.
+	 */
+	if(n_params > 0) {
+		char *message = g_strjoinv(" ", params);
+
+		g_message("Server sent the following SASL mechanisms: %s", message);
+
+		g_free(message);
+	} else {
+		g_message("Server sent an empty list of SASL mechanisms");
+	}
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_authenticate(G_GNUC_UNUSED GHashTable *tags,
+                               G_GNUC_UNUSED const char *source,
+                               G_GNUC_UNUSED const char *command,
+                               guint n_params,
+                               GStrv params,
+                               GError **error,
+                               gpointer user_data)
+{
+	PurpleIRCv3Connection *connection = user_data;
+	PurpleIRCv3SASLData *data = NULL;
+	char *payload = NULL;
+	gboolean done = FALSE;
+
+	if(n_params != 1) {
+		g_warning("ignoring AUTHENTICATE with %d parameters", n_params);
+
+		return TRUE;
+	}
+
+	payload = params[0];
+	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
+
+	/* If the server sent us a payload, combine the chunks. */
+	if(payload[0] != '+') {
+		if(data->server_in_buffer == NULL) {
+			data->server_in_buffer = g_string_new(payload);
+		} else {
+			g_string_append(data->server_in_buffer, payload);
+		}
+
+		if(strlen(payload) < 400) {
+			done = TRUE;
+		}
+	} else {
+		/* The server sent a + which is an empty message or the final message
+		 * ended on a 400 byte barrier. */
+		done = TRUE;
+	}
+
+	if(done) {
+		char *server_in = NULL;
+		char *client_out = NULL;
+		gsize server_in_length = 0;
+		size_t client_out_length = 0;
+		int res = 0;
+
+		/* If we have a buffer, base64 decode it, and then free it. */
+		if(data->server_in_buffer != NULL) {
+			server_in = (char *)g_base64_decode(data->server_in_buffer->str,
+			                                    &server_in_length);
+			g_string_free(data->server_in_buffer, TRUE);
+		}
+
+		/* Try to move to the next step of the sasl client. */
+		res = gsasl_step(data->session, server_in, server_in_length,
+		                 &client_out, &client_out_length);
+
+		/* We should be done with server_in, so free it.*/
+		g_clear_pointer(&server_in, g_free);
+
+		/* If we didn't get ok or continue, it's an error. */
+		if(res != GSASL_OK) {
+			g_set_error(error, PURPLE_CONNECTION_ERROR,
+			            PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+			            _("SASL authentication failed: %s"),
+			            gsasl_strerror(res));
+
+			return FALSE;
+		}
+
+		/* If we got an output for the client, write it out. */
+		if(client_out_length > 0) {
+			char *encoded = g_base64_encode((guchar *)client_out,
+			                                client_out_length);
+
+			purple_ircv3_connection_writef(connection, "AUTHENTICATE %s",
+			                               encoded);
+
+			g_free(encoded);
+		} else {
+			purple_ircv3_connection_writef(connection, "AUTHENTICATE +");
+		}
+	}
+
+	return TRUE;
+}
+
diff --git a/libpurple/protocols/ircv3/tests/meson.build b/libpurple/protocols/ircv3/tests/meson.build
--- a/libpurple/protocols/ircv3/tests/meson.build
+++ b/libpurple/protocols/ircv3/tests/meson.build
@@ -5,7 +5,7 @@
 foreach prog : TESTS
 	e = executable(
 		f'test_ircv3_@prog@', f'test_ircv3_@prog@.c',
-		dependencies : [libpurple_dep, glib],
+		dependencies : [libpurple_dep, glib, gsasl],
 		objects : ircv3_prpl.extract_all_objects())
 
 	test(f'ircv3_@prog@', e)
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -483,6 +483,8 @@
 #######################################################################
 sasl = dependency('libsasl2', version : '>= 2.0')
 
+gsasl = dependency('libgsasl', version : '>= 2.0')
+
 #AC_MSG_CHECKING(for me pot o' gold)
 #AC_MSG_RESULT(no)
 
