libpurple/protocols/ircv3/purpleircv3connection.c

Thu, 13 Oct 2022 23:49:22 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 13 Oct 2022 23:49:22 -0500
changeset 41811
f4ac61968453
parent 41807
66d473190e94
child 41812
c3cd920261b6
permissions
-rw-r--r--

Fix some bugs in the IRCv3 connection process

Testing Done:
Connected an IRCv3 account and verified I no longer got a `GWarning` about invalid property names.

Reviewed at https://reviews.imfreedom.org/r/1935/

/*
 * 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 "purpleircv3connection.h"

#include "purpleircv3core.h"
#include "purpleircv3parser.h"

enum {
	PROP_0,
	PROP_CANCELLABLE,
	PROP_CAPABILITIES,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _PurpleIRCv3Connection {
	PurpleConnection parent;

	GError *validate_error;

	GSocketConnection *connection;
	GCancellable *cancellable;

	gchar *server_name;

	GDataInputStream *input;
	PurpleQueuedOutputStream *output;

	PurpleIRCv3Parser *parser;

	char *capabilities;
};

G_DEFINE_DYNAMIC_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
                      PURPLE_TYPE_CONNECTION)

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_ircv3_connection_send_user_command(PurpleIRCv3Connection *connection) {
	PurpleAccount *account = NULL;
	const char *identname = NULL;
	const char *nickname = NULL;
	const char *realname = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	nickname = purple_connection_get_display_name(PURPLE_CONNECTION(connection));

	/* The stored value could be an empty string, so pass a default of empty
	 * string and then if it was empty, set our correct fallback.
	 */
	identname = purple_account_get_string(account, "ident", "");
	if(identname == NULL || *identname == '\0') {
		identname = nickname;
	}

	realname = purple_account_get_string(account, "real-name", "");
	if(realname == NULL || *realname == '\0') {
		realname = nickname;
	}

	purple_ircv3_connection_writef(connection, "USER %s 0 * :%s", identname,
	                               realname);
}

static void
purple_ircv3_connection_send_nick_command(PurpleIRCv3Connection *connection) {
	const char *nickname = NULL;

	nickname = purple_connection_get_display_name(PURPLE_CONNECTION(connection));

	purple_ircv3_connection_writef(connection, "NICK %s", nickname);
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
purple_ircv3_connection_read_cb(GObject *source, GAsyncResult *result,
                                gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	GDataInputStream *istream = G_DATA_INPUT_STREAM(source);
	GError *error = NULL;
	gchar *line = NULL;
	gsize length;

	line = g_data_input_stream_read_line_finish(istream, result, &length,
	                                            &error);
	if(line == NULL || error != NULL) {
		if(error == NULL) {
			g_set_error_literal(&error, PURPLE_CONNECTION_ERROR,
			                    PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
			                    _("Server closed the connection"));
		} else {
			g_prefix_error(&error, "%s", _("Lost connection with server: "));
		}

		purple_connection_take_error(PURPLE_CONNECTION(connection), error);

		/* In the off chance that line was returned, make sure we free it. */
		g_free(line);

		return;
	}

	purple_ircv3_parser_parse(connection->parser, line, &error, connection);

	g_free(line);

	/* Call read_line_async again to continue reading lines. */
	g_data_input_stream_read_line_async(connection->input,
	                                    G_PRIORITY_DEFAULT,
	                                    connection->cancellable,
	                                    purple_ircv3_connection_read_cb,
	                                    connection);
}

static void
purple_ircv3_connection_write_cb(GObject *source, GAsyncResult *result,
                                 gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleQueuedOutputStream *stream = PURPLE_QUEUED_OUTPUT_STREAM(source);
	GError *error = NULL;
	gboolean success = FALSE;

	success = purple_queued_output_stream_push_bytes_finish(stream, result,
	                                                        &error);

	if(!success) {
		purple_queued_output_stream_clear_queue(stream);

		g_prefix_error(&error, "%s", _("Lost connection with server: "));

		purple_connection_take_error(PURPLE_CONNECTION(connection), error);

		return;
	}
}

static void
purple_ircv3_connection_connected_cb(GObject *source, GAsyncResult *result,
                                     gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	GError *error = NULL;
	GInputStream *istream = NULL;
	GOutputStream *ostream = NULL;
	GSocketClient *client = G_SOCKET_CLIENT(source);
	GSocketConnection *conn = NULL;

	/* Finish the async method. */
	conn = g_socket_client_connect_to_host_finish(client, result, &error);
	if(conn == NULL || error != NULL) {
		g_prefix_error(&error, "%s", _("Unable to connect: "));

		purple_connection_take_error(PURPLE_CONNECTION(connection), error);

		return;
	}

	purple_connection_set_state(PURPLE_CONNECTION(connection),
	                            PURPLE_CONNECTION_CONNECTED);

	g_message("Successfully connected to %s", connection->server_name);

	/* Save our connection and setup our input and outputs. */
	connection->connection = conn;

	/* Create our parser. */
	connection->parser = purple_ircv3_parser_new();
	purple_ircv3_parser_add_default_handlers(connection->parser);

	ostream = g_io_stream_get_output_stream(G_IO_STREAM(conn));
	connection->output = purple_queued_output_stream_new(ostream);

	istream = g_io_stream_get_input_stream(G_IO_STREAM(conn));
	connection->input = g_data_input_stream_new(istream);
	g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(connection->input),
	                                     G_DATA_STREAM_NEWLINE_TYPE_CR_LF);

	/* Add our read callback. */
	g_data_input_stream_read_line_async(connection->input,
	                                    G_PRIORITY_DEFAULT,
	                                    connection->cancellable,
	                                    purple_ircv3_connection_read_cb,
	                                    connection);

	/* Send our registration commands. */
	purple_ircv3_connection_writef(connection, "CAP LS %s",
	                               PURPLE_IRCV3_CONNECTION_CAP_VERSION);
	purple_ircv3_connection_send_user_command(connection);
	purple_ircv3_connection_send_nick_command(connection);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
static void
purple_ircv3_connection_get_property(GObject *obj, guint param_id,
                                     GValue *value, GParamSpec *pspec)
{
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	switch(param_id) {
		case PROP_CANCELLABLE:
			g_value_set_object(value,
			                   purple_ircv3_connection_get_cancellable(connection));
			break;
		case PROP_CAPABILITIES:
			g_value_set_string(value,
			                   purple_ircv3_connection_get_capabilities(connection));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
purple_ircv3_connection_set_property(GObject *obj, guint param_id,
                                     const GValue *value, GParamSpec *pspec)
{
	switch(param_id) {
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
purple_ircv3_connection_dispose(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	g_clear_object(&connection->cancellable);

	g_clear_object(&connection->input);
	g_clear_object(&connection->output);
	g_clear_object(&connection->connection);

	g_clear_object(&connection->parser);

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
}

static void
purple_ircv3_connection_finalize(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	g_clear_error(&connection->validate_error);
	g_clear_pointer(&connection->server_name, g_free);

	g_clear_pointer(&connection->capabilities, g_free);

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->finalize(obj);
}

static void
purple_ircv3_connection_constructed(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
	PurpleAccount *account = NULL;
	gchar **userparts = NULL;
	const gchar *username = NULL;

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->constructed(obj);

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));

	/* Make sure the username (which includes the servername via usersplits),
	 * does not contain any whitespace.
	 */
	username = purple_account_get_username(account);
	if(strpbrk(username, " \t\v\r\n") != NULL) {
		g_set_error(&connection->validate_error,
		            PURPLE_CONNECTION_ERROR,
		            PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
		            _("IRC nick and server may not contain whitespace"));

		return;
	}

	/* Split the username into nick and server and store the values. */
	userparts = g_strsplit(username, "@", 2);
	purple_connection_set_display_name(PURPLE_CONNECTION(connection),
	                                   userparts[0]);
	connection->server_name = g_strdup(userparts[1]);
	g_strfreev(userparts);

	/* Finally create our cancellable. */
	connection->cancellable = g_cancellable_new();
}

static void
purple_ircv3_connection_init(PurpleIRCv3Connection *connection) {
}

static void
purple_ircv3_connection_class_finalize(PurpleIRCv3ConnectionClass *klass) {
}

static void
purple_ircv3_connection_class_init(PurpleIRCv3ConnectionClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->get_property = purple_ircv3_connection_get_property;
	obj_class->set_property = purple_ircv3_connection_set_property;
	obj_class->constructed = purple_ircv3_connection_constructed;
	obj_class->dispose = purple_ircv3_connection_dispose;
	obj_class->finalize = purple_ircv3_connection_finalize;

	/**
	 * PurpleIRCv3Connection:cancellable:
	 *
	 * The [class@Gio.Cancellable] for this connection.
	 *
	 * Since: 3.0.0
	 */
	properties[PROP_CANCELLABLE] = g_param_spec_object(
		"cancellable", "cancellable",
		"The cancellable for this connection",
		G_TYPE_CANCELLABLE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleIRCv3Connection:capabilities:
	 *
	 * The capabilities that the server supports.
	 *
	 * This is created during registration of the connection and is useful for
	 * troubleshooting or just reporting them to end users.
	 *
	 * Since: 3.0.0
	 */
	properties[PROP_CAPABILITIES] = g_param_spec_string(
		"capabilities", "capabilities",
		"The capabilities that the server supports",
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
void
purple_ircv3_connection_register(GPluginNativePlugin *plugin) {
	purple_ircv3_connection_register_type(G_TYPE_MODULE(plugin));
}

GCancellable *
purple_ircv3_connection_get_cancellable(PurpleIRCv3Connection *connection) {
	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);

	return connection->cancellable;
}

gboolean
purple_ircv3_connection_valid(PurpleIRCv3Connection *connection,
                              GError **error)
{
	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);

	if(connection->validate_error != NULL) {
		g_propagate_error(error, connection->validate_error);
		connection->validate_error = NULL;

		return FALSE;
	}

	return TRUE;
}

void
purple_ircv3_connection_connect(PurpleIRCv3Connection *connection) {
	PurpleAccount *account = NULL;
	GError *error = NULL;
	GSocketClient *client = NULL;
	gint default_port = PURPLE_IRCV3_DEFAULT_TLS_PORT;
	gint port = 0;
	gboolean use_tls = TRUE;

	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
	g_return_if_fail(connection->connection == NULL);

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	client = purple_gio_socket_client_new(account, &error);
	if(!G_IS_SOCKET_CLIENT(client)) {
		purple_connection_take_error(PURPLE_CONNECTION(connection), error);

		return;
	}

	/* Turn on TLS if requested. */
	use_tls = purple_account_get_bool(account, "use-tls", TRUE);
	g_socket_client_set_tls(client, use_tls);

	/* If TLS is not being used, set the default port to the plain port. */
	if(!use_tls) {
		default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
	}
	port = purple_account_get_int(account, "port", default_port);

	/* Finally start the async connection. */
	g_socket_client_connect_to_host_async(client, connection->server_name,
	                                      port, connection->cancellable,
	                                      purple_ircv3_connection_connected_cb,
	                                      connection);

	g_clear_object(&client);
}

void
purple_ircv3_connection_close(PurpleIRCv3Connection *connection) {
	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));

	/* TODO: send QUIT command. */

	/* Cancel the cancellable to tell everyone we're shutting down. */
	if(G_IS_CANCELLABLE(connection->cancellable)) {
		g_cancellable_cancel(connection->cancellable);

		g_clear_object(&connection->cancellable);
	}

	if(G_IS_SOCKET_CONNECTION(connection->connection)) {
		GInputStream *istream = G_INPUT_STREAM(connection->input);
		GOutputStream *ostream = G_OUTPUT_STREAM(connection->output);

		purple_gio_graceful_close(G_IO_STREAM(connection->connection),
		                          istream, ostream);
	}

	g_clear_object(&connection->input);
	g_clear_object(&connection->output);
	g_clear_object(&connection->connection);
}

void
purple_ircv3_connection_writef(PurpleIRCv3Connection *connection,
                               const char *format, ...)
{
	GBytes *bytes = NULL;
	GString *msg = NULL;
	va_list vargs;

	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
	g_return_if_fail(format != NULL);

	/* Create our string and append our format to it. */
	msg = g_string_new("");

	va_start(vargs, format);
	g_string_vprintf(msg, format, vargs);
	va_end(vargs);

	/* Next add the trailing carriage return line feed. */
	g_string_append(msg, "\r\n");

	/* Finally turn the string into bytes and send it! */
	bytes = g_bytes_new_take(msg->str, msg->len);
	g_string_free(msg, FALSE);

	purple_queued_output_stream_push_bytes_async(connection->output, bytes,
	                                             G_PRIORITY_DEFAULT,
	                                             connection->cancellable,
	                                             purple_ircv3_connection_write_cb,
	                                             connection);

	g_bytes_unref(bytes);
}

const char *
purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection) {
	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);

	return connection->capabilities;
}

void
purple_ircv3_connection_append_capabilities(PurpleIRCv3Connection *connection,
                                            const char *capabilities)
{
	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
	g_return_if_fail(capabilities != NULL);

	if(connection->capabilities == NULL) {
		connection->capabilities = g_strdup(capabilities);
	} else {
		char *tmp = connection->capabilities;

		connection->capabilities = g_strdup_printf("%s %s",
		                                           connection->capabilities,
		                                           capabilities);

		g_free(tmp);
	}

	g_object_notify_by_pspec(G_OBJECT(connection),
	                         properties[PROP_CAPABILITIES]);
}

mercurial