view src/protocols/jabber/jabber.c @ 8273:f24172f53650

[gaim-migrate @ 8997] This is Scott Lamb's eventloop abstraction patch. If it breaks things, Scott Lamb will be glad to take the punishment. If it doesn't, it should make integration into other event loops easier. Well, no, not easier, harder actually, but it'll be done more appropriately and less hackily.. er, hacky. Is hackily a word? committer: Tailor Script <tailor@pidgin.im>
author Christian Hammond <chipx86@chipx86.com>
date Tue, 17 Feb 2004 02:17:48 +0000
parents 22b3974dc1af
children dd6fe7d965aa
line wrap: on
line source

/*
 * gaim - Jabber Protocol Plugin
 *
 * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
#include "internal.h"

#include "account.h"
#include "accountopt.h"
#include "debug.h"
#include "message.h"
#include "multi.h"
#include "notify.h"
#include "prpl.h"
#include "request.h"
#include "server.h"
#include "util.h"

#include "auth.h"
#include "buddy.h"
#include "chat.h"
#include "iq.h"
#include "jutil.h"
#include "message.h"
#include "parser.h"
#include "presence.h"
#include "jabber.h"
#include "roster.h"
#include "xdata.h"

#define JABBER_CONNECT_STEPS (js->gsc ? 8 : 5)

static GaimPlugin *my_protocol = NULL;

static void jabber_stream_init(JabberStream *js)
{
	char *open_stream;

	open_stream = g_strdup_printf("<stream:stream to='%s' "
				          "xmlns='jabber:client' "
						  "xmlns:stream='http://etherx.jabber.org/streams' "
						  "version='1.0'>",
						  js->user->domain);

	jabber_send_raw(js, open_stream, -1);

	g_free(open_stream);
}

static void
jabber_session_initialized_cb(JabberStream *js, xmlnode *packet, gpointer data)
{
	const char *type = xmlnode_get_attrib(packet, "type");
	if(type && !strcmp(type, "result")) {
		jabber_stream_set_state(js, JABBER_STREAM_CONNECTED);
	} else {
		gaim_connection_error(js->gc, _("Error initializing session"));
	}
}

static void jabber_session_init(JabberStream *js)
{
	JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET);
	xmlnode *session;

	jabber_iq_set_callback(iq, jabber_session_initialized_cb, NULL);

	session = xmlnode_new_child(iq->node, "session");
	xmlnode_set_attrib(session, "xmlns", "urn:ietf:params:xml:ns:xmpp-session");

	jabber_iq_send(iq);
}

static void jabber_bind_result_cb(JabberStream *js, xmlnode *packet,
		gpointer data)
{
	/* XXX: check for errors, re-set our own js->user JID */

	jabber_session_init(js);
}

static void jabber_stream_features_parse(JabberStream *js, xmlnode *packet)
{
	if(xmlnode_get_child(packet, "mechanisms")) {
		jabber_auth_start(js, packet);
	} else if(xmlnode_get_child(packet, "bind")) {
		xmlnode *bind, *resource;
		JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET);
		bind = xmlnode_new_child(iq->node, "bind");
		xmlnode_set_attrib(bind, "xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
		resource = xmlnode_new_child(bind, "resource");
		xmlnode_insert_data(resource, js->user->resource, -1);

		jabber_iq_set_callback(iq, jabber_bind_result_cb, NULL);

		jabber_iq_send(iq);
	}
}

static void jabber_stream_handle_error(JabberStream *js, xmlnode *packet)
{
	xmlnode *textnode;
	char *error_text = NULL;
	const char *text;
	char *buf;

	if(xmlnode_get_child(packet, "bad-format")) {
		text = _("Bad Format");
	} else if(xmlnode_get_child(packet, "bad-namespace-prefix")) {
		text = _("Bad Namespace Prefix");
	} else if(xmlnode_get_child(packet, "conflict")) {
		js->gc->wants_to_die = TRUE;
		text = _("Resource Conflict");
	} else if(xmlnode_get_child(packet, "connection-timeout")) {
		text = _("Connection Timeout");
	} else if(xmlnode_get_child(packet, "host-gone")) {
		text = _("Host Gone");
	} else if(xmlnode_get_child(packet, "host-unknown")) {
		text = _("Host Unknown");
	} else if(xmlnode_get_child(packet, "improper-addressing")) {
		text = _("Improper Addressing");
	} else if(xmlnode_get_child(packet, "internal-server-error")) {
		text = _("Internal Server Error");
	} else if(xmlnode_get_child(packet, "invalid-id")) {
		text = _("Invalid ID");
	} else if(xmlnode_get_child(packet, "invalid-namespace")) {
		text = _("Invalid Namespace");
	} else if(xmlnode_get_child(packet, "invalid-xml")) {
		text = _("Invalid XML");
	} else if(xmlnode_get_child(packet, "nonmatching-hosts")) {
		text = _("Non-matching Hosts");
	} else if(xmlnode_get_child(packet, "not-authorized")) {
		text = _("Not Authorized");
	} else if(xmlnode_get_child(packet, "policy-violation")) {
		text = _("Policy Violation");
	} else if(xmlnode_get_child(packet, "remote-connection-failed")) {
		text = _("Remote Connection Failed");
	} else if(xmlnode_get_child(packet, "resource-constraint")) {
		text = _("Resource Constraint");
	} else if(xmlnode_get_child(packet, "restricted-xml")) {
		text = _("Restricted XML");
	} else if(xmlnode_get_child(packet, "see-other-host")) {
		text = _("See Other Host");
	} else if(xmlnode_get_child(packet, "system-shutdown")) {
		text = _("System Shutdown");
	} else if(xmlnode_get_child(packet, "undefined-condition")) {
		text = _("Undefined Condition");
	} else if(xmlnode_get_child(packet, "unsupported-encoding")) {
		text = _("Unsupported Encoding");
	} else if(xmlnode_get_child(packet, "unsupported-stanza-type")) {
		text = _("Unsupported Stanza Type");
	} else if(xmlnode_get_child(packet, "unsupported-version")) {
		text = _("Unsupported Version");
	} else if(xmlnode_get_child(packet, "xml-not-well-formed")) {
		text = _("XML Not Well Formed");
	} else {
		text = _("Stream Error");
	}

	if((textnode = xmlnode_get_child(packet, "text")))
		error_text = xmlnode_get_data(textnode);

	buf = g_strdup_printf("%s%s%s", text,
			error_text ? ": " : "",
			error_text ? error_text : "");
	gaim_connection_error(js->gc, buf);
	g_free(buf);
	if(error_text)
		g_free(error_text);
}

static void tls_init(JabberStream *js);

void jabber_process_packet(JabberStream *js, xmlnode *packet)
{
	if(!strcmp(packet->name, "iq")) {
		jabber_iq_parse(js, packet);
	} else if(!strcmp(packet->name, "presence")) {
		jabber_presence_parse(js, packet);
	} else if(!strcmp(packet->name, "message")) {
		jabber_message_parse(js, packet);
	} else if(!strcmp(packet->name, "stream:features")) {
		jabber_stream_features_parse(js, packet);
	} else if(!strcmp(packet->name, "stream:error")) {
		jabber_stream_handle_error(js, packet);
	} else if(!strcmp(packet->name, "challenge")) {
		if(js->state == JABBER_STREAM_AUTHENTICATING)
			jabber_auth_handle_challenge(js, packet);
	} else if(!strcmp(packet->name, "success")) {
		if(js->state == JABBER_STREAM_AUTHENTICATING)
			jabber_auth_handle_success(js, packet);
	} else if(!strcmp(packet->name, "failure")) {
		if(js->state == JABBER_STREAM_AUTHENTICATING)
			jabber_auth_handle_failure(js, packet);
	} else if(!strcmp(packet->name, "proceed")) {
		if(js->state == JABBER_STREAM_AUTHENTICATING && !js->gsc)
			tls_init(js);
	} else {
		gaim_debug(GAIM_DEBUG_WARNING, "jabber", "Unknown packet: %s\n",
				packet->name);
	}
}

void jabber_send_raw(JabberStream *js, const char *data, int len)
{
	int ret;

	/* because printing a tab to debug every minute gets old */
	if(strcmp(data, "\t"))
		gaim_debug(GAIM_DEBUG_MISC, "jabber", "Sending%s: %s\n", 
				js->gsc ? " (ssl)" : "", data);

	if(js->gsc) {
		ret = gaim_ssl_write(js->gsc, data, len == -1 ? strlen(data) : len);
	} else {
		if(js->fd < 0)
			return;
		ret = write(js->fd, data, len == -1 ? strlen(data) : len);
	}

	if(ret < 0)
		gaim_connection_error(js->gc, _("Write error"));

}

void jabber_send(JabberStream *js, xmlnode *packet)
{
	char *txt;
	int len;

	txt = xmlnode_to_str(packet, &len);
	jabber_send_raw(js, txt, len);
	g_free(txt);
}

static void jabber_keepalive(GaimConnection *gc)
{
	jabber_send_raw(gc->proto_data, "\t", -1);
}

static void
jabber_recv_cb_ssl(gpointer data, GaimSslConnection *gsc,
		GaimInputCondition cond)
{
	GaimConnection *gc = data;
	JabberStream *js = gc->proto_data;
	int len;
	static char buf[4096];

	if(!g_list_find(gaim_connections_get_all(), gc)) {
		gaim_ssl_close(gsc);
		return;
	}

	if((len = gaim_ssl_read(gsc, buf, sizeof(buf) - 1)) > 0) {
		buf[len] = '\0';
		gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (ssl)(%d): %s\n", len, buf);
		jabber_parser_process(js, buf, len);
	} else {
		gaim_connection_error(gc, _("Read Error"));
	}
}

static void
jabber_recv_cb(gpointer data, gint source, GaimInputCondition condition)
{
	GaimConnection *gc = data;
	JabberStream *js = gc->proto_data;
	int len;
	static char buf[4096];

	if(!g_list_find(gaim_connections_get_all(), gc))
		return;

	if((len = read(js->fd, buf, sizeof(buf) - 1)) > 0) {
		buf[len] = '\0';
		gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (%d): %s\n", len, buf);
		jabber_parser_process(js, buf, len);
	} else {
		gaim_connection_error(gc, _("Read Error"));
	}
}

static void
jabber_login_callback_ssl(gpointer data, GaimSslConnection *gsc,
		GaimInputCondition cond)
{
	GaimConnection *gc = data;
	JabberStream *js = gc->proto_data;

	if(!g_list_find(gaim_connections_get_all(), gc)) {
		gaim_ssl_close(gsc);
		return;
	}

	js->gsc = gsc;

	if(js->state == JABBER_STREAM_CONNECTING)
		jabber_send_raw(js, "<?xml version='1.0' ?>", -1);

	jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING);
	gaim_ssl_input_add(gsc, jabber_recv_cb_ssl, gc);
}


static void
jabber_login_callback(gpointer data, gint source, GaimInputCondition cond)
{
	GaimConnection *gc = data;
	JabberStream *js = gc->proto_data;

	if(!g_list_find(gaim_connections_get_all(), gc)) {
		close(source);
		return;
	}

	js->fd = source;

	if(js->state == JABBER_STREAM_CONNECTING)
		jabber_send_raw(js, "<?xml version='1.0' ?>", -1);

	jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING);
	gc->inpa = gaim_input_add(js->fd, GAIM_INPUT_READ, jabber_recv_cb, gc);
}

static void
jabber_ssl_connect_failure(GaimSslConnection *gsc, GaimSslErrorType error,
		gpointer data)
{
	GaimConnection *gc = data;

	switch(error) {
		case GAIM_SSL_HANDSHAKE_FAILED:
			gaim_connection_error(gc, _("SSL Handshake Failed"));
			break;
	}
}

static void tls_init(JabberStream *js)
{
	gaim_input_remove(js->gc->inpa);
	js->gc->inpa = 0;
	js->gsc = gaim_ssl_connect_fd(js->gc->account, js->fd,
			jabber_login_callback_ssl, jabber_ssl_connect_failure, js->gc);
}


static void
jabber_login(GaimAccount *account)
{
	int rc;
	GaimConnection *gc = gaim_account_get_connection(account);
	const char *connect_server = gaim_account_get_string(account,
			"connect_server", "");
	const char *server;
	JabberStream *js;

	gc->flags |= GAIM_CONNECTION_HTML;
	js = gc->proto_data = g_new0(JabberStream, 1);
	js->gc = gc;
	js->fd = -1;
	js->callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, g_free);
	js->buddies = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, (GDestroyNotify)jabber_buddy_free);
	js->chats = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, NULL);
	js->chat_servers = g_list_append(NULL, g_strdup("conference.jabber.org"));
	js->user = jabber_id_new(gaim_account_get_username(account));
	js->next_id = g_random_int();

	if(!js->user) {
		gaim_connection_error(gc, _("Invalid Jabber ID"));
		return;
	}

	if(!js->user->resource) {
		char *me;
		js->user->resource = g_strdup("Gaim");
		if(!js->user->node) {
			js->user->node = js->user->domain;
			js->user->domain = g_strdup("jabber.org");
		}
		me = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain,
				js->user->resource);
		gaim_account_set_username(account, me);
		g_free(me);
	}

	server = connect_server[0] ? connect_server : js->user->domain;

	jabber_stream_set_state(js, JABBER_STREAM_CONNECTING);


	if(gaim_account_get_bool(account, "old_ssl", FALSE)) {
		if(gaim_ssl_is_supported()) {
			js->gsc = gaim_ssl_connect(account, server,
					gaim_account_get_int(account, "port", 5222),
					jabber_login_callback_ssl, jabber_ssl_connect_failure, gc);
		} else {
			gaim_connection_error(gc, _("SSL support unavailable"));
		}
	}

	if(!js->gsc) {
		rc = gaim_proxy_connect(account, server,
				gaim_account_get_int(account, "port", 5222),
				jabber_login_callback, gc);

		if (rc != 0)
			gaim_connection_error(gc, _("Unable to create socket"));
	}
}

static gboolean
conn_close_cb(gpointer data)
{
	JabberStream *js = data;
	gaim_connection_destroy(js->gc);
	return FALSE;
}

static void
jabber_connection_schedule_close(JabberStream *js)
{
	gaim_timeout_add(0, conn_close_cb, js);
}

static void
jabber_registration_result_cb(JabberStream *js, xmlnode *packet, gpointer data)
{
	const char *type = xmlnode_get_attrib(packet, "type");
	char *buf;

	if(!strcmp(type, "result")) {
		buf = g_strdup_printf(_("Registration of %s@%s successful"),
				js->user->node, js->user->domain);
		gaim_notify_info(NULL, _("Registration Successful"),
				_("Registration Successful"), buf);
		g_free(buf);
	} else {
		char *error;
		xmlnode *y;

		if((y = xmlnode_get_child(packet, "error"))) {
			error = xmlnode_get_data(y);
		} else {
			error = g_strdup(_("Unknown Error"));
		}

		buf = g_strdup_printf(_("Registration of %s@%s failed: %s"),
				js->user->node, js->user->domain, error);
		gaim_notify_error(NULL, _("Registration Failed"),
				_("Registration Failed"), buf);
		g_free(buf);
		g_free(error);
	}
	jabber_connection_schedule_close(js);
}

static void
jabber_register_cb(JabberStream *js, GaimRequestFields *fields)
{
	GList *groups, *flds;
	xmlnode *query, *y;
	JabberIq *iq;
	char *username;

	iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register");
	query = xmlnode_get_child(iq->node, "query");

	for(groups = gaim_request_fields_get_groups(fields); groups;
			groups = groups->next) {
		for(flds = gaim_request_field_group_get_fields(groups->data);
				flds; flds = flds->next) {
			GaimRequestField *field = flds->data;
			const char *id = gaim_request_field_get_id(field);
			const char *value = gaim_request_field_string_get_value(field);

			if(!strcmp(id, "username")) {
				y = xmlnode_new_child(query, "username");
			} else if(!strcmp(id, "password")) {
				y = xmlnode_new_child(query, "password");
			} else if(!strcmp(id, "name")) {
				y = xmlnode_new_child(query, "name");
			} else if(!strcmp(id, "email")) {
				y = xmlnode_new_child(query, "email");
			} else if(!strcmp(id, "nick")) {
				y = xmlnode_new_child(query, "nick");
			} else if(!strcmp(id, "first")) {
				y = xmlnode_new_child(query, "first");
			} else if(!strcmp(id, "last")) {
				y = xmlnode_new_child(query, "last");
			} else if(!strcmp(id, "address")) {
				y = xmlnode_new_child(query, "address");
			} else if(!strcmp(id, "city")) {
				y = xmlnode_new_child(query, "city");
			} else if(!strcmp(id, "state")) {
				y = xmlnode_new_child(query, "state");
			} else if(!strcmp(id, "zip")) {
				y = xmlnode_new_child(query, "zip");
			} else if(!strcmp(id, "phone")) {
				y = xmlnode_new_child(query, "phone");
			} else if(!strcmp(id, "url")) {
				y = xmlnode_new_child(query, "url");
			} else if(!strcmp(id, "date")) {
				y = xmlnode_new_child(query, "date");
			} else {
				continue;
			}
			xmlnode_insert_data(y, value, -1);
			if(!strcmp(id, "username")) {
				if(js->user->node)
					g_free(js->user->node);
				js->user->node = g_strdup(value);
			}
		}
	}

	username = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain,
			js->user->resource);
	gaim_account_set_username(js->gc->account, username);
	g_free(username);

	jabber_iq_set_callback(iq, jabber_registration_result_cb, NULL);

	jabber_iq_send(iq);

}

static void
jabber_register_cancel_cb(JabberStream *js, GaimRequestFields *fields)
{
	jabber_connection_schedule_close(js);
}

static void jabber_register_x_data_cb(JabberStream *js, xmlnode *result, gpointer data)
{
	xmlnode *query;
	JabberIq *iq;

	iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register");
	query = xmlnode_get_child(iq->node, "query");

	xmlnode_insert_child(query, result);

	jabber_iq_set_callback(iq, jabber_registration_result_cb, NULL);
	jabber_iq_send(iq);
}

void jabber_register_parse(JabberStream *js, xmlnode *packet)
{
	if(js->registration) {
		GaimRequestFields *fields;
		GaimRequestFieldGroup *group;
		GaimRequestField *field;
		xmlnode *query, *x, *y;
		char *instructions;

		/* get rid of the login thingy */
		gaim_connection_set_state(js->gc, GAIM_CONNECTED);

		query = xmlnode_get_child(packet, "query");

		if(xmlnode_get_child(query, "registered")) {
			gaim_notify_error(NULL, _("Already Registered"),
					_("Already Registered"), NULL);
			jabber_connection_schedule_close(js);
			return;
		}

		for(x = xmlnode_get_child(packet, "x"); x; x = xmlnode_get_next_twin(x)) {
			const char *xmlns;
			if(!(xmlns = xmlnode_get_attrib(x, "xmlns")))
				continue;

			if(!strcmp(xmlns, "jabber:x:data")) {
				jabber_x_data_request(js, x, jabber_register_x_data_cb, NULL);
				return;
			}
		}

		/* XXX: if no jabber:x:data, but jabber:x:oob is there, use that */

		/* as a last resort, use the old jabber:iq:register syntax */

		fields = gaim_request_fields_new();
		group = gaim_request_field_group_new(NULL);
		gaim_request_fields_add_group(fields, group);

		field = gaim_request_field_string_new("username", _("Username"),
				js->user->node, FALSE);
		gaim_request_field_group_add_field(group, field);

		field = gaim_request_field_string_new("password", _("Password"),
				gaim_account_get_password(js->gc->account), FALSE);
		gaim_request_field_string_set_masked(field, TRUE);
		gaim_request_field_group_add_field(group, field);

		if(xmlnode_get_child(query, "name")) {
			field = gaim_request_field_string_new("name", _("Name"),
					gaim_account_get_alias(js->gc->account), FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "email")) {
			field = gaim_request_field_string_new("email", _("E-Mail"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "nick")) {
			field = gaim_request_field_string_new("nick", _("Nickname"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "first")) {
			field = gaim_request_field_string_new("first", _("First Name"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "last")) {
			field = gaim_request_field_string_new("last", _("Last Name"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "address")) {
			field = gaim_request_field_string_new("address", _("Address"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "city")) {
			field = gaim_request_field_string_new("city", _("City"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "state")) {
			field = gaim_request_field_string_new("state", _("State"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "zip")) {
			field = gaim_request_field_string_new("zip", _("Postal Code"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "phone")) {
			field = gaim_request_field_string_new("phone", _("Phone"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "url")) {
			field = gaim_request_field_string_new("url", _("URL"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}
		if(xmlnode_get_child(query, "date")) {
			field = gaim_request_field_string_new("date", _("Date"),
					NULL, FALSE);
			gaim_request_field_group_add_field(group, field);
		}

		if((y = xmlnode_get_child(query, "instructions")))
			instructions = xmlnode_get_data(y);
		else
			instructions = g_strdup(_("Please fill out the information below "
						"to register your new account."));

		gaim_request_fields(js->gc, _("Register New Jabber Account"),
				_("Register New Jabber Account"), instructions, fields,
				_("Register"), G_CALLBACK(jabber_register_cb),
				_("Cancel"), G_CALLBACK(jabber_register_cancel_cb), js);
	}
}

void jabber_register_start(JabberStream *js)
{
	JabberIq *iq;

	iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:register");
	jabber_iq_send(iq);
}

static void jabber_register_account(GaimAccount *account)
{
	GaimConnection *gc = gaim_account_get_connection(account);
	JabberStream *js;
	const char *connect_server = gaim_account_get_string(account,
			"connect_server", "");
	const char *server;
	int rc;

	js = gc->proto_data = g_new0(JabberStream, 1);
	js->gc = gc;
	js->registration = TRUE;
	js->callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, g_free);
	js->user = jabber_id_new(gaim_account_get_username(account));
	js->next_id = g_random_int();

	if(!js->user) {
		gaim_connection_error(gc, _("Invalid Jabber ID"));
		return;
	}

	if(!js->user->resource) {
		char *me;
		js->user->resource = g_strdup("Gaim");
		if(!js->user->node) {
			js->user->node = js->user->domain;
			js->user->domain = g_strdup("jabber.org");
		}
		me = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain,
				js->user->resource);
		gaim_account_set_username(account, me);
		g_free(me);
	}

	server = connect_server[0] ? connect_server : js->user->domain;

	jabber_stream_set_state(js, JABBER_STREAM_CONNECTING);

	if(gaim_account_get_bool(account, "old_ssl", FALSE)) {
		if(gaim_ssl_is_supported()) {
			js->gsc = gaim_ssl_connect(account, server,
					gaim_account_get_int(account, "port", 5222),
					jabber_login_callback_ssl, jabber_ssl_connect_failure, gc);
		} else {
			gaim_connection_error(gc, _("SSL support unavailable"));
		}
	}

	if(!js->gsc) {
		rc = gaim_proxy_connect(account, server,
				gaim_account_get_int(account, "port", 5222),
				jabber_login_callback, gc);

		if (rc != 0)
			gaim_connection_error(gc, _("Unable to create socket"));
	}
}

static void jabber_close(GaimConnection *gc)
{
	JabberStream *js = gc->proto_data;

	jabber_send_raw(js, "</stream:stream>", -1);

	if(js->gsc) {
		gaim_ssl_close(js->gsc);
	} else {
		if(js->gc->inpa)
			gaim_input_remove(js->gc->inpa);
		close(js->fd);
	}

	if(js->context)
		g_markup_parse_context_free(js->context);
	if(js->callbacks)
		g_hash_table_destroy(js->callbacks);
	if(js->buddies)
		g_hash_table_destroy(js->buddies);
	if(js->chats)
		g_hash_table_destroy(js->chats);
	while(js->chat_servers) {
		g_free(js->chat_servers->data);
		js->chat_servers = g_list_delete_link(js->chat_servers, js->chat_servers);
	}
	if(js->stream_id)
		g_free(js->stream_id);
	if(js->user)
		jabber_id_free(js->user);
	g_free(js);
}

void jabber_stream_set_state(JabberStream *js, JabberStreamState state)
{
	js->state = state;
	switch(state) {
		case JABBER_STREAM_OFFLINE:
			break;
		case JABBER_STREAM_CONNECTING:
			gaim_connection_update_progress(js->gc, _("Connecting"), 1,
					JABBER_CONNECT_STEPS);
			break;
		case JABBER_STREAM_INITIALIZING:
			gaim_connection_update_progress(js->gc, _("Initializing Stream"),
					js->gsc ? 5 : 2, JABBER_CONNECT_STEPS);
			jabber_stream_init(js);
			jabber_parser_setup(js);
			break;
		case JABBER_STREAM_AUTHENTICATING:
			gaim_connection_update_progress(js->gc, _("Authenticating"),
					js->gsc ? 6 : 3, JABBER_CONNECT_STEPS);
			if(js->protocol_version == JABBER_PROTO_0_9)  {
				if(js->registration)
					jabber_register_start(js);
				else
					jabber_auth_start_old(js);
			}
			break;
		case JABBER_STREAM_REINITIALIZING:
			gaim_connection_update_progress(js->gc, _("Re-initializing Stream"),
					6, JABBER_CONNECT_STEPS);
			jabber_stream_init(js);
			break;
		case JABBER_STREAM_CONNECTED:
			gaim_connection_set_state(js->gc, GAIM_CONNECTED);
			jabber_roster_request(js);
			jabber_presence_send(js->gc, js->gc->away_state, js->gc->away);
			jabber_iq_disco_server(js);
			serv_finish_login(js->gc);
			break;
	}
}

char *jabber_get_next_id(JabberStream *js)
{
	return g_strdup_printf("gaim%x", js->next_id++);
}


static void jabber_idle_set(GaimConnection *gc, int idle)
{
	JabberStream *js = gc->proto_data;

	js->idle = idle ? time(NULL) - idle : idle;
}

static const char *jabber_list_icon(GaimAccount *a, GaimBuddy *b)
{
	return "jabber";
}

static void jabber_list_emblems(GaimBuddy *b, char **se, char **sw,
		char **nw, char **ne)
{
	JabberStream *js;
	JabberBuddy *jb;

	if(!b->account->gc)
		return;
	js = b->account->gc->proto_data;
	jb = jabber_buddy_find(js, b->name, FALSE);

	if(!GAIM_BUDDY_IS_ONLINE(b)) {
		if(jb && jb->error_msg)
			*nw = "error";

		if(jb && (jb->subscription & JABBER_SUB_PENDING ||
					!(jb->subscription & JABBER_SUB_TO)))
			*se = "notauthorized";
		else
			*se = "offline";
	} else {
		switch (b->uc) {
			case JABBER_STATE_AWAY:
				*se = "away";
				break;
			case JABBER_STATE_CHAT:
				*se = "chat";
				break;
			case JABBER_STATE_XA:
				*se = "extendedaway";
				break;
			case JABBER_STATE_DND:
				*se = "dnd";
				break;
			case JABBER_STATE_ERROR:
				*se = "error";
				break;
		}
	}
}

static char *jabber_status_text(GaimBuddy *b)
{
	JabberBuddy *jb = jabber_buddy_find(b->account->gc->proto_data, b->name,
			FALSE);
	char *ret = NULL;

	if(jb && !GAIM_BUDDY_IS_ONLINE(b) && (jb->subscription & JABBER_SUB_PENDING || !(jb->subscription & JABBER_SUB_TO))) {
		ret = g_strdup(_("Not Authorized"));
	} else if(jb && !GAIM_BUDDY_IS_ONLINE(b) && jb->error_msg) {
		ret = g_strdup(jb->error_msg);
	} else {
		char *stripped;

		stripped = gaim_markup_strip_html(jabber_buddy_get_status_msg(jb));

		if(!stripped && b->uc & UC_UNAVAILABLE)
			stripped = g_strdup(jabber_get_state_string(b->uc));

		if(stripped) {
			ret = g_markup_escape_text(stripped, -1);
			g_free(stripped);
		}
	}

	return ret;
}

static char *jabber_tooltip_text(GaimBuddy *b)
{
	JabberBuddy *jb = jabber_buddy_find(b->account->gc->proto_data, b->name,
			FALSE);
	GString *ret = g_string_new("");
	char *r = NULL;

	if(jb) {
		JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, NULL);
		const char *sub;
		if(jb->subscription & JABBER_SUB_FROM) {
			if(jb->subscription & JABBER_SUB_TO)
				sub = _("Both");
			else if(jb->subscription & JABBER_SUB_PENDING)
				sub = _("From (To pending)");
			else
				sub = _("From");
		} else {
			if(jb->subscription & JABBER_SUB_TO)
				sub = _("To");
			else if(jb->subscription & JABBER_SUB_PENDING)
				sub = _("None (To pending)");
			else
				sub = _("None");
		}
		g_string_append_printf(ret, "<b>%s:</b> %s\n", _("Subscription"), sub);

		if(jbr) {
			char *text = NULL;
			if(jbr->status) {
				char *stripped;
				stripped = gaim_markup_strip_html(jbr->status);
				text = g_markup_escape_text(stripped, -1);
				g_free(stripped);
			}

			g_string_append_printf(ret, "<b>%s:</b> %s%s%s\n",
					_("Status"),
					jabber_get_state_string(jbr->state),
					text ? ": " : "",
					text ? text : "");
			if(text)
				g_free(text);
		} else if(!GAIM_BUDDY_IS_ONLINE(b) && jb->error_msg) {
			g_string_append_printf(ret, "<b>%s:</b> %s\n",
					_("Error"), jb->error_msg);
		}
	}

	if(ret->len > 0) {
		g_string_truncate(ret, ret->len-1);
		r = ret->str;
		g_string_free(ret, FALSE);
	} else {
		g_string_free(ret, TRUE);
	}

	return r;
}

static GList *jabber_away_states(GaimConnection *gc)
{
	JabberStream *js = gc->proto_data;
	GList *m = NULL;

	m = g_list_append(m, _("Online"));
	m = g_list_append(m, _("Chatty"));
	m = g_list_append(m, _("Away"));
	m = g_list_append(m, _("Extended Away"));
	m = g_list_append(m, _("Do Not Disturb"));
	if(js->protocol_version == JABBER_PROTO_0_9)
		m = g_list_append(m, _("Invisible"));
	m = g_list_append(m, GAIM_AWAY_CUSTOM);

	return m;
}

static void
jabber_password_change_result_cb(JabberStream *js, xmlnode *packet,
		gpointer data)
{
	const char *type;

	type = xmlnode_get_attrib(packet, "type");

	if(!strcmp(type, "result")) {
		gaim_notify_info(js->gc, _("Password Changed"), _("Password Changed"),
				_("Your password has been changed."));
	} else {
		xmlnode *error;
		char *buf, *error_txt = NULL;


		if((error = xmlnode_get_child(packet, "error")))
			error_txt = xmlnode_get_data(error);

		if(error_txt) {
			buf = g_strdup_printf(_("Error changing password: %s"),
					error_txt);
			g_free(error_txt);
		} else {
			buf = g_strdup(_("Unknown error occurred changing password"));
		}

		gaim_notify_error(js->gc, _("Error"), _("Error"), buf);
		g_free(buf);
	}
}

static void jabber_password_change_cb(JabberStream *js,
		GaimRequestFields *fields)
{
	const char *p1, *p2;
	JabberIq *iq;
	xmlnode *query, *y;

	p1 = gaim_request_fields_get_string(fields, "password1");
	p2 = gaim_request_fields_get_string(fields, "password2");

	if(strcmp(p1, p2)) {
		gaim_notify_error(js->gc, NULL, _("New passwords do not match."), NULL);
		return;
	}

	iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register");

	xmlnode_set_attrib(iq->node, "to", js->user->domain);

	query = xmlnode_get_child(iq->node, "query");

	y = xmlnode_new_child(query, "username");
	xmlnode_insert_data(y, js->user->node, -1);
	y = xmlnode_new_child(query, "password");
	xmlnode_insert_data(y, p1, -1);

	jabber_iq_set_callback(iq, jabber_password_change_result_cb, NULL);

	jabber_iq_send(iq);

	gaim_account_set_password(js->gc->account, p1);
}

static void jabber_password_change(GaimConnection *gc)
{
	JabberStream *js = gc->proto_data;
	GaimRequestFields *fields;
	GaimRequestFieldGroup *group;
	GaimRequestField *field;

	fields = gaim_request_fields_new();
	group = gaim_request_field_group_new(NULL);
	gaim_request_fields_add_group(fields, group);

	field = gaim_request_field_string_new("password1", _("Password"),
			"", FALSE);
	gaim_request_field_string_set_masked(field, TRUE);
	gaim_request_field_group_add_field(group, field);

	field = gaim_request_field_string_new("password2", _("Password (again)"),
			"", FALSE);
	gaim_request_field_string_set_masked(field, TRUE);
	gaim_request_field_group_add_field(group, field);

	gaim_request_fields(js->gc, _("Change Jabber Password"),
			_("Change Jabber Password"), _("Please enter your new password"),
			fields, _("OK"), G_CALLBACK(jabber_password_change_cb),
			_("Cancel"), NULL, js);
}

static GList *jabber_actions(GaimConnection *gc)
{
	JabberStream *js = gc->proto_data;
	GList *m = NULL;
	struct proto_actions_menu *pam;

	pam = g_new0(struct proto_actions_menu, 1);
	pam->label = _("Set User Info");
	pam->callback = jabber_setup_set_info;
	pam->gc = gc;
	m = g_list_append(m, pam);

	if(js->protocol_version == JABBER_PROTO_0_9) {
		pam = g_new0(struct proto_actions_menu, 1);
		pam->label = _("Change Password");
		pam->callback = jabber_password_change;
		pam->gc = gc;
		m = g_list_append(m, pam);
	}

	return m;
}

static GaimChat *jabber_find_blist_chat(GaimAccount *account, const char *name)
{
	GaimBlistNode *gnode, *cnode;
	JabberID *jid;

	if(!(jid = jabber_id_new(name)))
		return NULL;

	for(gnode = gaim_get_blist()->root; gnode; gnode = gnode->next) {
		for(cnode = gnode->child; cnode; cnode = cnode->next) {
			GaimChat *chat = (GaimChat*)cnode;
			const char *room, *server;
			if(!GAIM_BLIST_NODE_IS_CHAT(cnode))
				continue;

			if(chat->account != account)
				continue;

			if(!(room = g_hash_table_lookup(chat->components, "room")))
				continue;
			if(!(server = g_hash_table_lookup(chat->components, "server")))
				continue;

			if(jid->node && jid->domain &&
					!g_utf8_collate(room, jid->node) && !g_utf8_collate(server, jid->domain)) {
				jabber_id_free(jid);
				return chat;
			}
		}
	}
	jabber_id_free(jid);
	return NULL;
}

static GaimPluginProtocolInfo prpl_info =
{
	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME,
	NULL,
	NULL,
	NULL,
	jabber_list_icon,
	jabber_list_emblems,
	jabber_status_text,
	jabber_tooltip_text,
	jabber_away_states,
	jabber_actions,
	jabber_buddy_menu,
	jabber_chat_info,
	jabber_login,
	jabber_close,
	jabber_message_send_im,
	jabber_set_info,
	jabber_send_typing,
	jabber_buddy_get_info,
	jabber_presence_send,
	NULL,
	NULL,
	NULL,
	NULL,
	jabber_idle_set,
	NULL,
	jabber_roster_add_buddy,
	NULL,
	jabber_roster_remove_buddy,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	jabber_chat_join,
	jabber_chat_invite,
	jabber_chat_leave,
	NULL,
	jabber_message_send_chat,
	jabber_keepalive,
	jabber_register_account,
	jabber_buddy_get_info_chat,
	NULL,
	jabber_roster_alias_change,
	jabber_roster_group_change,
	jabber_roster_group_rename,
	NULL,
	NULL, /* convo_closed */ /* XXX: thread_ids */
	jabber_normalize,
	NULL, /* set_buddy_icon */
	NULL, /* remove_group */
	jabber_chat_buddy_real_name,
	jabber_chat_set_topic,
	jabber_find_blist_chat,
	jabber_roomlist_get_list,
	jabber_roomlist_cancel,
	NULL
};

static GaimPluginInfo info =
{
	2,                                                /**< api_version    */
	GAIM_PLUGIN_PROTOCOL,                             /**< type           */
	NULL,                                             /**< ui_requirement */
	0,                                                /**< flags          */
	NULL,                                             /**< dependencies   */
	GAIM_PRIORITY_DEFAULT,                            /**< priority       */

	"prpl-jabber",                                    /**< id             */
	"Jabber",                                         /**< name           */
	VERSION,                                          /**< version        */
	                                                  /**  summary        */
	N_("Jabber Protocol Plugin"),
	                                                  /**  description    */
	N_("Jabber Protocol Plugin"),
	NULL,                                             /**< author         */
	GAIM_WEBSITE,                                     /**< homepage       */

	NULL,                                             /**< load           */
	NULL,                                             /**< unload         */
	NULL,                                             /**< destroy        */

	NULL,                                             /**< ui_info        */
	&prpl_info                                        /**< extra_info     */
};

static void
init_plugin(GaimPlugin *plugin)
{
	GaimAccountUserSplit *split;
	GaimAccountOption *option;
	struct proto_pref *ppref;

	split = gaim_account_user_split_new(_("Server"), "jabber.org", '@');
	prpl_info.user_splits = g_list_append(prpl_info.user_splits, split);

	split = gaim_account_user_split_new(_("Resource"), "Gaim", '/');
	prpl_info.user_splits = g_list_append(prpl_info.user_splits, split);

	option = gaim_account_option_bool_new(_("Use TLS if available"), "use_tls",
			TRUE);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
			option);

	option = gaim_account_option_bool_new(_("Force old SSL"), "old_ssl", FALSE);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
			option);

	option = gaim_account_option_bool_new(
			_("Allow plaintext auth over unencrypted streams"),
			"auth_plain_in_clear", FALSE);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
			option);

	option = gaim_account_option_int_new(_("Port"), "port", 5222);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
			option);

	option = gaim_account_option_string_new(_("Connect server"),
			"connect_server", NULL);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
			option);

	my_protocol = plugin;

	gaim_prefs_add_none("/plugins/prpl/jabber");
	gaim_prefs_add_bool("/plugins/prpl/jabber/hide_os", FALSE);

	ppref = g_new0(struct proto_pref, 1);
	ppref->key = NULL;
	ppref->label = _("Privacy");
	prpl_info.protocol_prefs = g_list_append(prpl_info.protocol_prefs, ppref);

	ppref = g_new0(struct proto_pref, 1);
	ppref->key = "/plugins/prpl/jabber/hide_os";
	ppref->label = _("Hide Operating System"); /* XXX: come up with a better name for this */
	prpl_info.protocol_prefs = g_list_append(prpl_info.protocol_prefs, ppref);
}

GAIM_INIT_PLUGIN(jabber, init_plugin, info);