changeset 8262:b5dbd1839716

[gaim-migrate @ 8985] this is jabber new-style file transfer receive support. this doesn't do much error checking or handling, but I managed to send pictures to myself from Exodus on my laptop in the living room, which would have taken twice as long were it not for VNC. i said i was going to bed 1, 2, and 3 hours ago. i should go to bed. committer: Tailor Script <tailor@pidgin.im>
author Nathan Walp <nwalp@pidgin.im>
date Sun, 15 Feb 2004 10:11:38 +0000
parents 7b57c3bd9db6
children c095f0dcbece
files src/ft.c src/protocols/jabber/iq.c src/protocols/jabber/oob.c src/protocols/jabber/si.c src/protocols/jabber/si.h src/proxy.c src/proxy.h src/xmlnode.c src/xmlnode.h
diffstat 9 files changed, 373 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/src/ft.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/ft.c	Sun Feb 15 10:11:38 2004 +0000
@@ -500,12 +500,8 @@
 
 	if (condition & GAIM_INPUT_READ) {
 		r = gaim_xfer_read(xfer, &buffer);
-		if (r > 0) {
+		if (r > 0)
 			fwrite(buffer, 1, r, xfer->dest_fp);
-		} else {
-			gaim_xfer_cancel_remote(xfer);
-			return;
-		}
 	}
 	else {
 		size_t s = MIN(gaim_xfer_get_bytes_remaining(xfer), 4096);
--- a/src/protocols/jabber/iq.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/protocols/jabber/iq.c	Sun Feb 15 10:11:38 2004 +0000
@@ -267,17 +267,16 @@
 		SUPPORT_FEATURE("jabber:iq:time")
 		SUPPORT_FEATURE("jabber:iq:version")
 		SUPPORT_FEATURE("jabber:x:conference")
-		/*
 		SUPPORT_FEATURE("http://jabber.org/protocol/bytestreams")
-		*/
 		SUPPORT_FEATURE("http://jabber.org/protocol/disco#info")
 		SUPPORT_FEATURE("http://jabber.org/protocol/disco#items")
+#if 0
+		SUPPORT_FEATURE("http://jabber.org/protocol/ibb")
+#endif
 		SUPPORT_FEATURE("http://jabber.org/protocol/muc")
 		SUPPORT_FEATURE("http://jabber.org/protocol/muc#user")
-		/*
 		SUPPORT_FEATURE("http://jabber.org/protocol/si")
 		SUPPORT_FEATURE("http://jabber.org/protocol/si/profile/file-transfer")
-		*/
 		SUPPORT_FEATURE("http://jabber.org/protocol/xhtml-im")
 
 		jabber_iq_send(iq);
@@ -411,6 +410,9 @@
 			} else if(!strcmp(xmlns, "jabber:iq:oob")) {
 				jabber_oob_parse(js, packet);
 				return;
+			} else if(!strcmp(xmlns, "http://jabber.org/protocol/bytestreams")) {
+				jabber_bytestreams_parse(js, packet);
+				return;
 			}
 		} else if(!strcmp(type, "get")) {
 			if(!strcmp(xmlns, "jabber:iq:last")) {
@@ -441,6 +443,13 @@
 				return;
 			}
 		}
+	} else {
+		xmlnode *si;
+		if((si = xmlnode_get_child(packet, "si")) && (xmlns = xmlnode_get_attrib(si, "xmlns")) &&
+				!strcmp(xmlns, "http://jabber.org/protocol/si")) {
+			jabber_si_parse(js, packet);
+			return;
+		}
 	}
 
 	/* If we got here, no pre-defined handlers got it, lets see if a special
--- a/src/protocols/jabber/oob.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/protocols/jabber/oob.c	Sun Feb 15 10:11:38 2004 +0000
@@ -117,9 +117,6 @@
 	return 0;
 }
 
-static void jabber_oob_xfer_cancel_send(GaimXfer *xfer) {
-}
-
 static void jabber_oob_xfer_cancel_recv(GaimXfer *xfer) {
 	JabberOOBXfer *jox = xfer->data;
 	JabberIq *iq;
@@ -173,7 +170,6 @@
 
 	gaim_xfer_set_init_fnc(xfer,   jabber_oob_xfer_init);
 	gaim_xfer_set_end_fnc(xfer,    jabber_oob_xfer_end);
-	gaim_xfer_set_cancel_send_fnc(xfer, jabber_oob_xfer_cancel_send);
 	gaim_xfer_set_cancel_recv_fnc(xfer, jabber_oob_xfer_cancel_recv);
 	gaim_xfer_set_read_fnc(xfer,   jabber_oob_xfer_read);
 	gaim_xfer_set_start_fnc(xfer,  jabber_oob_xfer_start);
--- a/src/protocols/jabber/si.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/protocols/jabber/si.c	Sun Feb 15 10:11:38 2004 +0000
@@ -23,6 +23,7 @@
 #include "ft.h"
 #include "network.h"
 #include "notify.h"
+#include "sha.h"
 #include "util.h"
 
 #include "buddy.h"
@@ -32,39 +33,308 @@
 
 #include "si.h"
 
-static GaimXfer *jabber_si_xfer_find_by_id(JabberStream *js, const char *id)
+struct bytestreams_streamhost {
+	char *jid;
+	char *host;
+	int port;
+};
+
+typedef struct _JabberSIXfer {
+	JabberStream *js;
+
+	char *stream_id;
+	char *iq_id;
+
+	enum {
+		STREAM_METHOD_UNKNOWN = 0,
+		STREAM_METHOD_BYTESTREAMS = 2 << 1,
+		STREAM_METHOD_IBB = 2 << 2,
+		STREAM_METHOD_UNSUPPORTED = 2 << 31
+	} stream_method;
+
+	GList *streamhosts;
+	GaimProxyInfo *gpi;
+} JabberSIXfer;
+
+static GaimXfer*
+jabber_si_xfer_find(JabberStream *js, const char *sid, const char *from)
 {
 	GList *xfers;
 
-	if(!id)
+	if(!sid || !from)
 		return NULL;
 
 	for(xfers = js->file_transfers; xfers; xfers = xfers->next) {
 		GaimXfer *xfer = xfers->data;
 		JabberSIXfer *jsx = xfer->data;
-
-		if(!strcmp(jsx->id, id))
+		if(!strcmp(jsx->stream_id, sid) && !strcmp(xfer->who, from))
 			return xfer;
 	}
 
 	return NULL;
 }
 
-static void
-jabber_si_xfer_ibb_start(JabberStream *js, xmlnode *packet, gpointer data) {
+
+static void jabber_si_bytestreams_attempt_connect(GaimXfer *xfer);
+
+static void jabber_si_bytestreams_connect_cb(gpointer data, gint source, GaimInputCondition cond)
+{
 	GaimXfer *xfer = data;
 	JabberSIXfer *jsx = xfer->data;
+	JabberIq *iq;
+	xmlnode *query, *su;
+	struct bytestreams_streamhost *streamhost = jsx->streamhosts->data;
 
-	/* Make sure we didn't get an error back */
+	gaim_proxy_info_destroy(jsx->gpi);
+
+	if(source < 0) {
+		jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
+		g_free(streamhost->jid);
+		g_free(streamhost->host);
+		g_free(streamhost);
+		jabber_si_bytestreams_attempt_connect(xfer);
+		return;
+	}
+
+	iq = jabber_iq_new_query(jsx->js, JABBER_IQ_RESULT, "http://jabber.org/protocol/bytestreams");
+	xmlnode_set_attrib(iq->node, "to", xfer->who);
+	jabber_iq_set_id(iq, jsx->iq_id);
+	query = xmlnode_get_child(iq->node, "query");
+	su = xmlnode_new_child(query, "streamhost-used");
+	xmlnode_set_attrib(su, "jid", streamhost->jid);
+
+	jabber_iq_send(iq);
+
+	gaim_xfer_start(xfer, source, NULL, -1);
+}
+
+static void jabber_si_bytestreams_attempt_connect(GaimXfer *xfer)
+{
+	JabberSIXfer *jsx = xfer->data;
+	struct bytestreams_streamhost *streamhost;
+	char *dstaddr, *p;
+	int i;
+	unsigned char hashval[20];
+
+	if(!jsx->streamhosts) {
+		JabberIq *iq = jabber_iq_new(jsx->js, JABBER_IQ_ERROR);
+		xmlnode *error, *condition;
+
+		if(jsx->iq_id)
+			jabber_iq_set_id(iq, jsx->iq_id);
+
+		xmlnode_set_attrib(iq->node, "to", xfer->who);
+		error = xmlnode_new_child(iq->node, "error");
+		xmlnode_set_attrib(error, "code", "404");
+		xmlnode_set_attrib(error, "type", "cancel");
+		condition = xmlnode_new_child(error, "condition");
+		xmlnode_set_attrib(condition, "xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas");
+		xmlnode_new_child(condition, "item-not-found");
+
+		jabber_iq_send(iq);
+
+		gaim_xfer_cancel_local(xfer);
+
+		return;
+	}
+
+	streamhost = jsx->streamhosts->data;
 
-	/* XXX: OK, here we need to set up a g_idle thing to send messages
-	 * until our eyes bleed, but do it without interfering with normal
-	 * gaim operations.  When we're done, we have to send a <close> like
-	 * we sent the <open> to start this damn thing.  If we're really
-	 * fortunate, Exodus or someone else will implement something to test
-	 * against soon */
+	jsx->gpi = gaim_proxy_info_new();
+	gaim_proxy_info_set_type(jsx->gpi, GAIM_PROXY_SOCKS5);
+	gaim_proxy_info_set_host(jsx->gpi, streamhost->host);
+	gaim_proxy_info_set_port(jsx->gpi, streamhost->port);
+
+	dstaddr = g_strdup_printf("%s%s%s@%s/%s", jsx->stream_id, xfer->who, jsx->js->user->node,
+			jsx->js->user->domain, jsx->js->user->resource);
+	shaBlock((unsigned char *)dstaddr, strlen(dstaddr), hashval);
+	g_free(dstaddr);
+	dstaddr = g_malloc(41);
+	p = dstaddr;
+	for(i=0; i<20; i++, p+=2)
+		snprintf(p, 3, "%02x", hashval[i]);
+
+	gaim_proxy_connect_socks5(jsx->gpi, dstaddr, 0, jabber_si_bytestreams_connect_cb, xfer);
+	g_free(dstaddr);
+}
+
+void jabber_bytestreams_parse(JabberStream *js, xmlnode *packet)
+{
+	GaimXfer *xfer;
+	JabberSIXfer *jsx;
+	xmlnode *query, *streamhost;
+	const char *sid, *from;
+
+	if(!(from = xmlnode_get_attrib(packet, "from")))
+		return;
+
+	if(!(query = xmlnode_get_child(packet, "query")))
+		return;
+
+	if(!(sid = xmlnode_get_attrib(query, "sid")))
+		return;
+
+	if(!(xfer = jabber_si_xfer_find(js, sid, from)))
+		return;
+
+	jsx = xfer->data;
+	if(jsx->iq_id)
+		g_free(jsx->iq_id);
+	jsx->iq_id = g_strdup(xmlnode_get_attrib(packet, "id"));
+
+	for(streamhost = xmlnode_get_child(query, "streamhost"); streamhost;
+			streamhost = xmlnode_get_next_twin(streamhost)) {
+		const char *jid, *host, *port;
+		int portnum;
+
+		if((jid = xmlnode_get_attrib(streamhost, "jid")) &&
+				(host = xmlnode_get_attrib(streamhost, "host")) &&
+				(port = xmlnode_get_attrib(streamhost, "port")) &&
+				(portnum = atoi(port))) {
+			struct bytestreams_streamhost *sh = g_new0(struct bytestreams_streamhost, 1);
+			sh->jid = g_strdup(jid);
+			sh->host = g_strdup(host);
+			sh->port = portnum;
+			jsx->streamhosts = g_list_append(jsx->streamhosts, sh);
+		}
+	}
+
+	jabber_si_bytestreams_attempt_connect(xfer);
 }
 
+static void jabber_si_xfer_init(GaimXfer *xfer)
+{
+	JabberSIXfer *jsx = xfer->data;
+	JabberIq *iq;
+	xmlnode *si, *feature, *x, *field, *value;
+
+	iq = jabber_iq_new(jsx->js, JABBER_IQ_RESULT);
+	xmlnode_set_attrib(iq->node, "to", xfer->who);
+	if(jsx->iq_id)
+		jabber_iq_set_id(iq, jsx->iq_id);
+
+	si = xmlnode_new_child(iq->node, "si");
+	xmlnode_set_attrib(si, "xmlns", "http://jabber.org/protocol/si");
+
+	feature = xmlnode_new_child(si, "feature");
+	xmlnode_set_attrib(feature, "xmlns", "http://jabber.org/protocol/feature-neg");
+
+	x = xmlnode_new_child(feature, "x");
+	xmlnode_set_attrib(x, "xmlns", "jabber:x:data");
+	xmlnode_set_attrib(x, "type", "form");
+
+	field = xmlnode_new_child(x, "field");
+	xmlnode_set_attrib(field, "var", "stream-method");
+
+	value = xmlnode_new_child(field, "value");
+	if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS)
+		xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1);
+	/*
+	else if(jsx->stream_method & STREAM_METHOD_IBB)
+		xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1);
+		*/
+
+	jabber_iq_send(iq);
+}
+
+static ssize_t jabber_si_xfer_read(char **buffer, GaimXfer *xfer) {
+	char buf;
+
+	if(read(xfer->fd, &buf, 1) == 1) {
+		if(buf == 0x00)
+			gaim_xfer_set_read_fnc(xfer, NULL);
+	} else {
+		gaim_debug_error("jabber", "Read error on bytestream transfer!\n");
+		gaim_xfer_cancel_local(xfer);
+	}
+
+	return 0;
+}
+
+
+void jabber_si_parse(JabberStream *js, xmlnode *packet)
+{
+	JabberSIXfer *jsx;
+	GaimXfer *xfer;
+	xmlnode *si, *file, *feature, *x, *field, *option, *value;
+	const char *stream_id, *filename, *filesize_c, *profile;
+	size_t filesize = 0;
+
+	if(!(si = xmlnode_get_child(packet, "si")))
+		return;
+
+	if(!(profile = xmlnode_get_attrib(si, "profile")) ||
+			strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer"))
+		return;
+
+	if(!(stream_id = xmlnode_get_attrib(si, "id")))
+		return;
+
+	if(!(file = xmlnode_get_child(si, "file")))
+		return;
+
+	if(!(filename = xmlnode_get_attrib(file, "name")))
+		return;
+
+	if((filesize_c = xmlnode_get_attrib(file, "size")))
+		filesize = atoi(filesize_c);
+
+	if(!(feature = xmlnode_get_child(si, "feature")))
+		return;
+
+	if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data")))
+		return;
+
+	jsx = g_new0(JabberSIXfer, 1);
+
+	for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) {
+		const char *var = xmlnode_get_attrib(field, "var");
+		if(var && !strcmp(var, "stream-method")) {
+			for(option = xmlnode_get_child(field, "option"); option;
+					option = xmlnode_get_next_twin(option)) {
+				if((value = xmlnode_get_child(option, "value"))) {
+					char *val;
+					if((val = xmlnode_get_data(value))) {
+						if(!strcmp(val, "http://jabber.org/protocol/bytestreams")) {
+							jsx->stream_method |= STREAM_METHOD_BYTESTREAMS;
+							/*
+						} else if(!strcmp(val, "http://jabber.org/protocol/ibb")) {
+							jsx->stream_method |= STREAM_METHOD_IBB;
+							*/
+						}
+						g_free(val);
+					}
+				}
+			}
+		}
+	}
+
+	if(jsx->stream_method == STREAM_METHOD_UNKNOWN) {
+		g_free(jsx);
+		return;
+	}
+
+	jsx->js = js;
+	jsx->stream_id = g_strdup(stream_id);
+	jsx->iq_id = g_strdup(xmlnode_get_attrib(packet, "id"));
+
+	xfer = gaim_xfer_new(js->gc->account, GAIM_XFER_RECEIVE,
+			xmlnode_get_attrib(packet, "from"));
+	xfer->data = jsx;
+
+	gaim_xfer_set_filename(xfer, filename);
+	if(filesize > 0)
+		gaim_xfer_set_size(xfer, filesize);
+
+	gaim_xfer_set_init_fnc(xfer, jabber_si_xfer_init);
+	gaim_xfer_set_read_fnc(xfer, jabber_si_xfer_read);
+
+	js->file_transfers = g_list_append(js->file_transfers, xfer);
+
+	gaim_xfer_request(xfer);
+}
+
+#if 0
 void jabber_si_parse(JabberStream *js, xmlnode *packet)
 {
 	GaimXfer *xfer;
@@ -267,3 +537,4 @@
 	gaim_debug(GAIM_DEBUG_INFO, "jabber", "in jabber_si_xfer_ack\n");
 }
 
+#endif
--- a/src/protocols/jabber/si.h	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/protocols/jabber/si.h	Sun Feb 15 10:11:38 2004 +0000
@@ -26,28 +26,7 @@
 
 #include "jabber.h"
 
-typedef struct _JabberSIXfer {
-	JabberStream *js;
-
-	char *id;
-	char *resource;
-
-	enum {
-		STREAM_METHOD_UNKNOWN,
-		STREAM_METHOD_BYTESTREAMS,
-		STREAM_METHOD_IBB,
-		STREAM_METHOD_UNSUPPORTED
-	} stream_method;
-} JabberSIXfer;
-
+void jabber_bytestreams_parse(JabberStream *js, xmlnode *packet);
 void jabber_si_parse(JabberStream *js, xmlnode *packet);
 
-void jabber_si_xfer_init(GaimXfer *xfer);
-void jabber_si_xfer_start(GaimXfer *xfer);
-void jabber_si_xfer_end(GaimXfer *xfer);
-void jabber_si_xfer_cancel_send(GaimXfer *xfer);
-void jabber_si_xfer_cancel_recv(GaimXfer *xfer);
-void jabber_si_xfer_ack(GaimXfer *xfer, const char *buffer, size_t size);
-
-
 #endif /* _GAIM_JABBER_SI_H_ */
--- a/src/proxy.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/proxy.c	Sun Feb 15 10:11:38 2004 +0000
@@ -1314,7 +1314,7 @@
 	gaim_input_remove(phb->inpa);
 	gaim_debug(GAIM_DEBUG_INFO, "socks5 proxy", "Able to read again.\n");
 
-	if (read(source, buf, 10) < 10) {
+	if (read(source, buf, 4) < 4) {
 		gaim_debug(GAIM_DEBUG_WARNING, "socks5 proxy", "or not...\n");
 		close(source);
 
@@ -1346,6 +1346,25 @@
 		return;
 	}
 
+	/* Skip past BND.ADDR */
+	switch(buf[3]) {
+		case 0x01: /* the address is a version-4 IP address, with a length of 4 octets */
+			lseek(source, 4, SEEK_CUR);
+			break;
+		case 0x03: /* the address field contains a fully-qualified domain name.  The first
+					  octet of the address field contains the number of octets of name that
+					  follow, there is no terminating NUL octet. */
+			read(source, buf+4, 1);
+			lseek(source, buf[4], SEEK_CUR);
+			break;
+		case 0x04: /* the address is a version-6 IP address, with a length of 16 octets */
+			lseek(source, 16, SEEK_CUR);
+			break;
+	}
+
+	/* Skip past BND.PORT */
+	lseek(source, 2, SEEK_CUR);
+
 	if (phb->account == NULL ||
 		gaim_account_get_connection(phb->account) != NULL) {
 
@@ -1763,6 +1782,24 @@
 									connection_host_resolved, phb);
 }
 
+int
+gaim_proxy_connect_socks5(GaimProxyInfo *gpi, const char *host, int port,
+		GaimInputFunction func, gpointer data)
+{
+	struct PHB *phb;
+
+	phb = g_new0(struct PHB, 1);
+	phb->gpi = gpi;
+	phb->func = func;
+	phb->data = data;
+	phb->host = g_strdup(host);
+	phb->port = port;
+
+	return gaim_gethostbyname_async(gaim_proxy_info_get_host(gpi), gaim_proxy_info_get_port(gpi),
+									connection_host_resolved, phb);
+}
+
+
 static void
 proxy_pref_cb(const char *name, GaimPrefType type, gpointer value,
 			  gpointer data)
--- a/src/proxy.h	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/proxy.h	Sun Feb 15 10:11:38 2004 +0000
@@ -238,6 +238,20 @@
 int gaim_proxy_connect(GaimAccount *account, const char *host, int port,
 					   GaimInputFunction func, gpointer data);
 
+/**
+ * Makes a connection through a SOCKS5 proxy.
+ *
+ * @param gpi     The GaimProxyInfo specifying the proxy settings
+ * @param host    The destination host.
+ * @param port    The destination port.
+ * @param func    The input handler function.
+ * @param data    User-defined data.
+ *
+ * @return The socket handle.
+ */
+int gaim_proxy_connect_socks5(GaimProxyInfo *gpi, const char *host, int port,
+					   GaimInputFunction func, gpointer data);
+
 /*@}*/
 
 #ifdef __cplusplus
--- a/src/xmlnode.c	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/xmlnode.c	Sun Feb 15 10:11:38 2004 +0000
@@ -183,7 +183,7 @@
 }
 
 xmlnode*
-xmlnode_get_child(xmlnode *parent, const char *name)
+xmlnode_get_child_with_namespace(xmlnode *parent, const char *name, const char *ns)
 {
 	xmlnode *x, *ret = NULL;
 	char **names;
@@ -196,19 +196,30 @@
 	child_name = names[1];
 
 	for(x = parent->child; x; x = x->next) {
-		if(x->type == XMLNODE_TYPE_TAG && name && !strcmp(parent_name, x->name)) {
+		const char *xmlns = NULL;
+		if(ns)
+			xmlns = xmlnode_get_attrib(x, "xmlns");
+
+		if(x->type == XMLNODE_TYPE_TAG && name && !strcmp(parent_name, x->name)
+				&& (!ns || (xmlns && !strcmp(ns, xmlns)))) {
 			ret = x;
 			break;
 		}
 	}
 
 	if(child_name && ret)
-		ret = xmlnode_get_child(x, child_name);
+		ret = xmlnode_get_child(ret, child_name);
 
 	g_strfreev(names);
 	return ret;
 }
 
+xmlnode*
+xmlnode_get_child(xmlnode *parent, const char *name)
+{
+	return xmlnode_get_child_with_namespace(parent, name, NULL);
+}
+
 char *
 xmlnode_get_data(xmlnode *node)
 {
@@ -416,12 +427,18 @@
 
 xmlnode *xmlnode_get_next_twin(xmlnode *node) {
 	xmlnode *sibling;
+	const char *ns = xmlnode_get_attrib(node, "xmlns");
 
 	g_return_val_if_fail(node != NULL, NULL);
 	g_return_val_if_fail(node->type == XMLNODE_TYPE_TAG, NULL);
 
 	for(sibling = node->next; sibling; sibling = sibling->next) {
-		if(sibling->type == XMLNODE_TYPE_TAG && !strcmp(node->name, sibling->name))
+		const char *xmlns;
+		if(ns)
+			xmlns = xmlnode_get_attrib(sibling, "xmlns");
+
+		if(sibling->type == XMLNODE_TYPE_TAG && !strcmp(node->name, sibling->name) &&
+				(!ns || (xmlns && !strcmp(ns, xmlns))))
 			return sibling;
 	}
 
--- a/src/xmlnode.h	Sun Feb 15 08:51:32 2004 +0000
+++ b/src/xmlnode.h	Sun Feb 15 10:11:38 2004 +0000
@@ -46,6 +46,7 @@
 xmlnode *xmlnode_new_child(xmlnode *parent, const char *name);
 void xmlnode_insert_child(xmlnode *parent, xmlnode *child);
 xmlnode *xmlnode_get_child(xmlnode *parent, const char *name);
+xmlnode *xmlnode_get_child_with_namespace(xmlnode *parent, const char *name, const char *xmlns);
 xmlnode *xmlnode_get_next_twin(xmlnode *node);
 void xmlnode_insert_data(xmlnode *parent, const char *data, size_t size);
 char *xmlnode_get_data(xmlnode *node);