changeset 25988:f36a94f19db3

Restore BOSH to a more-or-less working state. Major changes: * Obey the 'requests' attribute on session creation response. Extra requests are buffered until we can send something. * Attempt to gracefully fail from a proxy that doesn't allow pipelining to multiple TCP connections. Still to do: * SSL the Pidgin<>Connection Manager connection * Pay attention to 'inactivity' and 'polling' * The HTTP handler won't work if a read() on a pipelined connection returns data from one response as well as the beginning of a second response.
author Paul Aurich <paul@darkrain42.org>
date Sun, 15 Mar 2009 04:48:47 +0000
parents 035ba9355361
children 1c450685dbc6
files libpurple/protocols/jabber/bosh.c
diffstat 1 files changed, 208 insertions(+), 113 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/protocols/jabber/bosh.c	Sat Mar 07 02:23:14 2009 +0000
+++ b/libpurple/protocols/jabber/bosh.c	Sun Mar 15 04:48:47 2009 +0000
@@ -28,7 +28,8 @@
 
 #include "bosh.h"
 
-typedef struct _PurpleHTTPRequest PurpleHTTPRequest;
+#define MAX_HTTP_CONNECTIONS      2
+
 typedef struct _PurpleHTTPConnection PurpleHTTPConnection;
 
 typedef void (*PurpleHTTPConnectionConnectFunction)(PurpleHTTPConnection *conn);
@@ -38,6 +39,12 @@
 
 static char *bosh_useragent = NULL;
 
+typedef enum {
+	PACKET_TERMINATE,
+	PACKET_STREAM_RESTART,
+	PACKET_NORMAL,
+} PurpleBOSHPacketType;
+
 struct _PurpleBOSHConnection {
     /* decoded URL */
     char *host;
@@ -51,8 +58,12 @@
 
     JabberStream *js;
     gboolean pipelining;
-    PurpleHTTPConnection *conn_a;
-    PurpleHTTPConnection *conn_b;
+	PurpleHTTPConnection *connections[MAX_HTTP_CONNECTIONS];
+
+	int max_inactivity;
+	int max_requests;
+	int requests;
+	GString *pending;
 
     gboolean ready;
     PurpleBOSHConnectionConnectFunction connect_cb;
@@ -61,10 +72,11 @@
 
 struct _PurpleHTTPConnection {
     int fd;
+	gboolean ready;
     char *host;
     int port;
     int ie_handle;
-    int requests; /* number of outstanding HTTP requests */
+	int requests; /* number of outstanding HTTP requests */
 
     GString *buf;
     gboolean headers_done;
@@ -77,19 +89,15 @@
     PurpleBOSHConnection *bosh;
 };
 
-struct _PurpleHTTPRequest {
-    const char *path;
-    char *data;
-    int data_len;
-};
-
 static void jabber_bosh_connection_stream_restart(PurpleBOSHConnection *conn);
 static gboolean jabber_bosh_connection_error_check(PurpleBOSHConnection *conn, xmlnode *node);
 static void jabber_bosh_connection_received(PurpleBOSHConnection *conn, xmlnode *node);
-static void jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, xmlnode *node);
+static void jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, PurpleBOSHPacketType, xmlnode *node);
 
 static void jabber_bosh_http_connection_connect(PurpleHTTPConnection *conn);
-static void jabber_bosh_http_connection_send_request(PurpleHTTPConnection *conn, PurpleHTTPRequest *req);
+static void jabber_bosh_connection_connected(PurpleHTTPConnection *conn);
+static void jabber_bosh_http_connection_disconnected(PurpleHTTPConnection *conn);
+static void jabber_bosh_http_connection_send_request(PurpleHTTPConnection *conn, const GString *req);
 
 void jabber_bosh_init(void)
 {
@@ -116,20 +124,15 @@
 	bosh_useragent = NULL;
 }
 
-static void
-jabber_bosh_http_request_destroy(PurpleHTTPRequest *req)
-{
-	g_free(req->data);
-	g_free(req);
-}
-
 static PurpleHTTPConnection*
-jabber_bosh_http_connection_init(const char *host, int port)
+jabber_bosh_http_connection_init(PurpleBOSHConnection *bosh)
 {
 	PurpleHTTPConnection *conn = g_new0(PurpleHTTPConnection, 1);
-	conn->host = g_strdup(host);
-	conn->port = port;
+	conn->bosh = bosh;
+	conn->host = g_strdup(bosh->host);
+	conn->port = bosh->port;
 	conn->fd = -1;
+	conn->ready = FALSE;
 
 	return conn;
 }
@@ -180,9 +183,12 @@
 	conn->js = js;
 	/* FIXME: This doesn't seem very random */
 	conn->rid = rand() % 100000 + 1728679472;
+
+	conn->pending = g_string_new("");
+
 	conn->ready = FALSE;
-	conn->conn_a = jabber_bosh_http_connection_init(conn->host, conn->port);
-	conn->conn_a->bosh = conn;
+
+	conn->connections[0] = jabber_bosh_http_connection_init(conn);
 
 	return conn;
 }
@@ -190,47 +196,29 @@
 void
 jabber_bosh_connection_destroy(PurpleBOSHConnection *conn)
 {
+	int i;
+
 	g_free(conn->host);
 	g_free(conn->path);
 
-	if (conn->conn_a)
-		jabber_bosh_http_connection_destroy(conn->conn_a);
-	if (conn->conn_b)
-		jabber_bosh_http_connection_destroy(conn->conn_b);
+	if (conn->pending)
+		g_string_free(conn->pending, TRUE);
+
+	for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) {
+		if (conn->connections[i])
+			jabber_bosh_http_connection_destroy(conn->connections[i]);
+	}
 
 	g_free(conn);
 }
 
 void jabber_bosh_connection_close(PurpleBOSHConnection *conn)
 {
-	xmlnode *packet = xmlnode_new("body");
-	/* XEP-0124: The rid must not exceed 16 characters */
-	char rid[17];
-	g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid);
-	xmlnode_set_attrib(packet, "type", "terminate");
-	xmlnode_set_attrib(packet, "xmlns", "http://jabber.org/protocol/httpbind");
-	xmlnode_set_attrib(packet, "sid", conn->sid);
-	xmlnode_set_attrib(packet, "rid", rid);
-
-	jabber_bosh_connection_send_native(conn, packet);
-	xmlnode_free(packet);
+	jabber_bosh_connection_send_native(conn, PACKET_TERMINATE, NULL);
 }
 
 static void jabber_bosh_connection_stream_restart(PurpleBOSHConnection *conn) {
-	xmlnode *restart = xmlnode_new("body");
-	/* XEP-0124: The rid must not exceed 16 characters */
-	char rid[17];
-	g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid);
-	xmlnode_set_attrib(restart, "rid", rid);
-	xmlnode_set_attrib(restart, "sid", conn->sid);
-	xmlnode_set_attrib(restart, "to", conn->js->user->domain);
-	xmlnode_set_attrib(restart, "xml:lang", "en");
-	xmlnode_set_attrib(restart, "xmpp:restart", "true");
-	xmlnode_set_attrib(restart, "xmlns", "http://jabber.org/protocol/httpbind");
-	xmlnode_set_attrib(restart, "xmlns:xmpp", "urn:xmpp:xbosh"); 
-	
-	jabber_bosh_connection_send_native(conn, restart);
-	xmlnode_free(restart);
+	jabber_bosh_connection_send_native(conn, PACKET_STREAM_RESTART, NULL);
 }
 
 static gboolean jabber_bosh_connection_error_check(PurpleBOSHConnection *conn, xmlnode *node) {
@@ -242,7 +230,7 @@
 		conn->ready = FALSE;
 		purple_connection_error_reason (conn->js->gc,
 			PURPLE_CONNECTION_ERROR_OTHER_ERROR,
-			_("The BOSH conncetion manager suggested to terminate your session."));
+			_("The BOSH connection manager terminated your session."));
 		return TRUE;
 	}
 	return FALSE;
@@ -303,6 +291,7 @@
 
 static void boot_response_cb(PurpleBOSHConnection *conn, xmlnode *node) {
 	const char *sid, *version;
+	const char *inactivity, *requests;
 	xmlnode *packet;
 
 	g_return_if_fail(node != NULL);
@@ -312,6 +301,9 @@
 	sid = xmlnode_get_attrib(node, "sid");
 	version = xmlnode_get_attrib(node, "ver");
 
+	inactivity = xmlnode_get_attrib(node, "inactivity");
+	requests = xmlnode_get_attrib(node, "requests");
+
 	if (sid) {
 		conn->sid = g_strdup(sid);
 	} else {
@@ -338,6 +330,12 @@
 		purple_debug_info("jabber", "Missing version in BOSH initiation\n");
 	}
 
+	if (inactivity)
+		conn->max_inactivity = atoi(inactivity);
+
+	if (requests)
+		conn->max_requests = atoi(requests);
+
 	/* FIXME: Depending on receiving features might break with some hosts */
 	packet = xmlnode_get_child(node, "features");
 	conn->js->use_bosh = TRUE;
@@ -345,30 +343,61 @@
 	jabber_stream_features_parse(conn->js, packet);		
 }
 
+static PurpleHTTPConnection *
+find_available_http_connection(PurpleBOSHConnection *conn)
+{
+	int i;
+
+	/* Easy solution: Does everyone involved support pipelining? Hooray! Just use
+	 * one TCP connection! */
+	if (conn->pipelining)
+		return conn->connections[0];
+
+	/* First loop, look for a connection that's ready */
+	for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) {
+		if (conn->connections[i] && conn->connections[i]->ready &&
+		                            conn->connections[i]->requests == 0)
+			return conn->connections[i];
+	}
+
+	/* Second loop, look for one that's NULL and create a new connection */
+	for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) {
+		if (!conn->connections[i]) {
+			conn->connections[i] = jabber_bosh_http_connection_init(conn);
+
+			conn->connections[i]->connect_cb = jabber_bosh_connection_connected;
+			conn->connections[i]->disconnect_cb = jabber_bosh_http_connection_disconnected;
+			jabber_bosh_http_connection_connect(conn->connections[i]);
+			return NULL;
+		}
+	}
+
+	/* None available. */
+	return NULL;
+}
+
 static void jabber_bosh_connection_boot(PurpleBOSHConnection *conn) {
-	xmlnode *init = xmlnode_new("body");
-	/* XEP-0124: The rid must not exceed 16 characters */
-	char rid[17];
-	g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid);
-	xmlnode_set_attrib(init, "content", "text/xml; charset=utf-8");
-	xmlnode_set_attrib(init, "secure", "true");
-/*
-	xmlnode_set_attrib(init, "route", tmp = g_strdup_printf("xmpp:%s:5222", conn->js->user->domain));
-	g_free(tmp);
-*/
-	xmlnode_set_attrib(init, "to", conn->js->user->domain);
-	xmlnode_set_attrib(init, "xml:lang", "en");
-	xmlnode_set_attrib(init, "xmpp:version", "1.0");
-	xmlnode_set_attrib(init, "ver", "1.6");
-	xmlnode_set_attrib(init, "xmlns:xmpp", "urn:xmpp:xbosh"); 
-	xmlnode_set_attrib(init, "rid", rid);
-	xmlnode_set_attrib(init, "wait", "60"); /* this should be adjusted automatically according to real time network behavior */
-	xmlnode_set_attrib(init, "xmlns", "http://jabber.org/protocol/httpbind");
-	xmlnode_set_attrib(init, "hold", "1");
-	
+	GString *buf = g_string_new("");
+
+	g_string_printf(buf, "<body content='text/xml; charset=utf-8' "
+	                "secure='true' "
+	                "to='%s' "
+	                "xml:lang='en' "
+	                "xmpp:version='1.0' "
+	                "ver='1.6' "
+	                "xmlns:xmpp='urn:xmpp:bosh' "
+	                "rid='%" G_GUINT64_FORMAT "' "
+/* TODO: This should be adjusted/adjustable automatically according to
+ * realtime network behavior */
+	                "wait='60' "
+	                "hold='1' "
+	                "xmlns='http://jabber.org/protocol/httpbind'/>",
+	                conn->js->user->domain,
+	                ++conn->rid);
+
 	conn->receive_cb = boot_response_cb;
-	jabber_bosh_connection_send_native(conn, init);
-	xmlnode_free(init);
+	jabber_bosh_http_connection_send_request(conn->connections[0], buf);
+	g_string_free(buf, TRUE);
 }
 
 static void
@@ -391,22 +420,7 @@
 }
 
 void jabber_bosh_connection_send(PurpleBOSHConnection *conn, xmlnode *node) {
-	xmlnode *packet = xmlnode_new("body");
-	/* XEP-0124: The rid must not exceed 16 characters */
-	char rid[17];
-	g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid);
-	xmlnode_set_attrib(packet, "xmlns", "http://jabber.org/protocol/httpbind");
-	xmlnode_set_attrib(packet, "sid", conn->sid);
-	xmlnode_set_attrib(packet, "rid", rid);
-	
-	if (node) {
-		xmlnode *copy = xmlnode_copy(node);
-		xmlnode_insert_child(packet, copy);
-		if (conn->ready == TRUE)
-			xmlnode_set_attrib(copy, "xmlns", "jabber:client");
-	}
-	jabber_bosh_connection_send_native(conn, packet);
-	xmlnode_free(packet);
+	jabber_bosh_connection_send_native(conn, PACKET_NORMAL, node);
 }
 
 void jabber_bosh_connection_send_raw(PurpleBOSHConnection *conn,
@@ -414,7 +428,7 @@
 {
 	xmlnode *node = xmlnode_from_str(data, len);
 	if (node) {
-		jabber_bosh_connection_send(conn, node);
+		jabber_bosh_connection_send_native(conn, PACKET_NORMAL, node);
 		xmlnode_free(node);
 	} else {
 		/*
@@ -427,21 +441,88 @@
 	}
 }
 
-static void jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, xmlnode *node) {
-	PurpleHTTPRequest *request;
+static void
+jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, PurpleBOSHPacketType type,
+                                   xmlnode *node)
+{
+	PurpleHTTPConnection *chosen;
+	GString *packet = NULL;
+	char *buf = NULL;
+
+	chosen = find_available_http_connection(conn);
+
+	if (type != PACKET_NORMAL && !chosen) {
+		/*
+		 * For non-ordinary traffic, we don't want to 'buffer' it, so use the first
+		 * connection.
+		 */
+		chosen = conn->connections[0];
 	
-	request = g_new0(PurpleHTTPRequest, 1);
-	request->path = conn->path;
-	request->data = xmlnode_to_str(node, &(request->data_len));
+		if (!chosen->ready)
+			purple_debug_warning("jabber", "First BOSH connection wasn't ready. Bad "
+					"things may happen.\n");
+	}
+
+	if (node)
+		buf = xmlnode_to_str(node, NULL);
+
+	if (type == PACKET_NORMAL && (!chosen ||
+	        (conn->max_requests > 0 && conn->requests == conn->max_requests))) {
+		/*
+		 * For normal data, send up to max_requests requests at a time or there is no
+		 * connection ready (likely, we're currently opening a second connection and
+		 * will send these packets when connected).
+		 */
+		if (buf) {
+			conn->pending = g_string_append(conn->pending, buf);
+			g_free(buf);
+		}
+		return;
+	}
+
+	packet = g_string_new("");
 
-	jabber_bosh_http_connection_send_request(conn->conn_a, request);
+	g_string_printf(packet, "<body "
+	                "rid='%" G_GUINT64_FORMAT "' "
+	                "sid='%s' "
+	                "to='%s' "
+	                "xml:lang='en' "
+	                "xmlns='http://jabber.org/protocol/httpbind' "
+	                "xmlns:xmpp='urn:xmpp:xbosh'",
+	                ++conn->rid,
+	                conn->sid,
+	                conn->js->user->domain);
+
+	if (type == PACKET_STREAM_RESTART)
+		packet = g_string_append(packet, " xmpp:restart='true'/>");
+	else {
+		if (type == PACKET_TERMINATE)
+			packet = g_string_append(packet, " type='terminate'");
+
+		g_string_append_printf(packet, ">%s%s</body>", conn->pending->str,
+		                       buf ? buf : "");
+		g_string_truncate(conn->pending, 0);
+	}
+
+	g_free(buf);
+
+	jabber_bosh_http_connection_send_request(chosen, packet);
 }
 
 static void jabber_bosh_connection_connected(PurpleHTTPConnection *conn) {
-	if (conn->bosh->ready == TRUE && conn->bosh->connect_cb) {
+	conn->ready = TRUE;
+
+	if (conn->bosh->ready) {
 		purple_debug_info("jabber", "BOSH session already exists. Trying to reuse it.\n");
+		if (conn->bosh->pending && conn->bosh->pending->len > 0) {
+			/* Send the pending data */
+			jabber_bosh_connection_send_native(conn->bosh, PACKET_NORMAL, NULL);
+		}
+#if 0
 		conn->bosh->receive_cb = jabber_bosh_connection_received;
-		conn->bosh->connect_cb(conn->bosh);
+		if (conn->bosh->connect_cb)
+			conn->bosh->connect_cb(conn->bosh);
+#endif
 	} else
 		jabber_bosh_connection_boot(conn->bosh);
 }
@@ -452,13 +533,26 @@
 }
 
 static void jabber_bosh_http_connection_disconnected(PurpleHTTPConnection *conn) {
+	/*
+	 * Well, then. Fine! I never liked you anyway, server! I was cheating on you
+	 * with AIM!
+	 */
+	conn->ready = FALSE;
+
+	if (conn->bosh->pipelining)
+		/* Hmmmm, fall back to multiple connections */
+		conn->bosh->pipelining = FALSE;
+
+	/* No! Please! Take me back. It was me, not you! I was weak! */
 	conn->connect_cb = jabber_bosh_connection_connected;
 	jabber_bosh_http_connection_connect(conn);
 }
 
-void jabber_bosh_connection_connect(PurpleBOSHConnection *conn) {
-	conn->conn_a->connect_cb = jabber_bosh_connection_connected;
-	jabber_bosh_http_connection_connect(conn->conn_a);
+void jabber_bosh_connection_connect(PurpleBOSHConnection *bosh) {
+	PurpleHTTPConnection *conn = bosh->connections[0];
+	conn->connect_cb = jabber_bosh_connection_connected;
+	conn->disconnect_cb = jabber_bosh_http_connection_disconnected;
+	jabber_bosh_http_connection_connect(conn);
 }
 
 static void
@@ -501,9 +595,10 @@
 		return;
 
 	--conn->requests;
+	--conn->bosh->requests;
 
 #warning For a pure HTTP 1.1 stack, this would need to be handled elsewhere.
-	if (conn->bosh->ready && conn->requests == 0) {
+	if (conn->bosh->ready && conn->bosh->requests == 0) {
 		jabber_bosh_connection_send(conn->bosh, NULL);
 		purple_debug_misc("jabber", "BOSH: Sending an empty request\n");
 	}
@@ -528,7 +623,7 @@
 
 	purple_debug_info("jabber", "jabber_bosh_http_connection_read\n");
 
-	if (conn->buf == NULL)
+	if (!conn->buf)
 		conn->buf = g_string_new("");
 
 	while ((cnt = read(fd, buffer, sizeof(buffer))) > 0) {
@@ -599,20 +694,20 @@
 
 static void
 jabber_bosh_http_connection_send_request(PurpleHTTPConnection *conn,
-                                         PurpleHTTPRequest *req)
+                                         const GString *req)
 {
 	GString *packet = g_string_new("");
 	int ret;
 
-	/* TODO: Should we lie about this HTTP/1.1 support? */
-	g_string_append_printf(packet, "POST %s HTTP/1.1\r\n"
+	g_string_printf(packet, "POST %s HTTP/1.1\r\n"
 	                       "Host: %s\r\n"
 	                       "User-Agent: %s\r\n"
 	                       "Content-Encoding: text/xml; charset=utf-8\r\n"
-	                       "Content-Length: %d\r\n\r\n",
-	                       req->path, conn->host, bosh_useragent, req->data_len);
+	                       "Content-Length: %" G_GSIZE_FORMAT "\r\n\r\n",
+	                       conn->bosh->path, conn->host, bosh_useragent,
+	                       req->len);
 
-	packet = g_string_append(packet, req->data);
+	packet = g_string_append(packet, req->str);
 
 	purple_debug_misc("jabber", "BOSH out: %s\n", packet->str);
 	/* TODO: Better error handling, circbuffer or possible integration with
@@ -620,8 +715,8 @@
 	ret = write(conn->fd, packet->str, packet->len);
 
 	++conn->requests;
+	++conn->bosh->requests;
 	g_string_free(packet, TRUE);
-	jabber_bosh_http_request_destroy(req);
 
 	if (ret < 0 && errno == EAGAIN)
 		purple_debug_warning("jabber", "BOSH write would have blocked\n");