1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/libxml2dom/xmpp.py Sun Apr 08 00:01:29 2007 +0000
1.3 @@ -0,0 +1,479 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +XMPP support using libxml2dom to capture stanzas as documents. The XMPP
1.8 +specification employs an "open" or unfinished document as the basis for
1.9 +communications between client and server - this presents problems for
1.10 +DOM-oriented libraries.
1.11 +
1.12 +Various Internet standards specifications exist for XMPP.
1.13 +See: http://www.xmpp.org/rfcs/rfc3920.html
1.14 +See: http://www.xmpp.org/rfcs/rfc3921.html
1.15 +
1.16 +Copyright (C) 2007 Paul Boddie <paul@boddie.org.uk>
1.17 +
1.18 +This library is free software; you can redistribute it and/or
1.19 +modify it under the terms of the GNU Lesser General Public
1.20 +License as published by the Free Software Foundation; either
1.21 +version 2.1 of the License, or (at your option) any later version.
1.22 +
1.23 +This library is distributed in the hope that it will be useful,
1.24 +but WITHOUT ANY WARRANTY; without even the implied warranty of
1.25 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1.26 +Lesser General Public License for more details.
1.27 +
1.28 +You should have received a copy of the GNU Lesser General Public
1.29 +License along with this library; if not, write to the Free Software
1.30 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
1.31 +
1.32 +--------
1.33 +
1.34 +The process of connecting, authenticating, and so on is quite convoluted:
1.35 +
1.36 +s = libxml2dom.xmpp.Session(("localhost", 5222))
1.37 +d = s.connect("host")
1.38 +auth = s.createAuth() # provides access to the stanza
1.39 +auth.mechanism = "PLAIN" # choose a supported mechanism
1.40 +auth.setCredentials(jid, username, password) # for PLAIN authentication only
1.41 +d = s.send(auth) # hopefully a success response
1.42 +d = s.connect("host") # have to reconnect!
1.43 +iq = s.createIq() # make an 'iq' stanza
1.44 +iq.makeBind() # set up a binding operation
1.45 +d = s.send(iq) # hopefully a success response
1.46 +iq = s.createIq() # make an 'iq' stanza
1.47 +iq.makeSession() # set up a session
1.48 +d = s.send(iq) # hopefully a success response
1.49 +
1.50 +See tests/xmpp_test.py for more details.
1.51 +"""
1.52 +
1.53 +import libxml2dom
1.54 +from libxml2dom.macrolib import *
1.55 +from libxml2dom.macrolib import \
1.56 + createDocument as Node_createDocument
1.57 +import socket
1.58 +import select
1.59 +import base64 # for auth elements
1.60 +
1.61 +XMPP_BIND_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-bind"
1.62 +XMPP_CLIENT_NAMESPACE = "jabber:client"
1.63 +XMPP_REGISTER_NAMESPACE = "jabber:iq:register"
1.64 +XMPP_SASL_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"
1.65 +XMPP_SESSION_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-session"
1.66 +XMPP_STREAMS_NAMESPACE = "http://etherx.jabber.org/streams"
1.67 +
1.68 +class XMPPImplementation(libxml2dom.Implementation):
1.69 +
1.70 + "Contains an XMPP-specific implementation."
1.71 +
1.72 + # Wrapping of documents.
1.73 +
1.74 + def adoptDocument(self, node):
1.75 + return XMPPDocument(node, self)
1.76 +
1.77 + # Factory functions.
1.78 +
1.79 + def get_node(self, _node, context_node):
1.80 +
1.81 + """
1.82 + Get a libxml2dom node for the given low-level '_node' and libxml2dom
1.83 + 'context_node'.
1.84 + """
1.85 +
1.86 + if Node_nodeType(_node) == context_node.ELEMENT_NODE:
1.87 +
1.88 + # Make special binding elements.
1.89 +
1.90 + if Node_namespaceURI(_node) == XMPP_BIND_NAMESPACE:
1.91 + if Node_localName(_node) == "bind":
1.92 + return XMPPBindElement(_node, self, context_node.ownerDocument)
1.93 +
1.94 + # Make special client elements.
1.95 +
1.96 + elif Node_namespaceURI(_node) == XMPP_CLIENT_NAMESPACE:
1.97 + if Node_localName(_node) == "iq":
1.98 + return XMPPIqElement(_node, self, context_node.ownerDocument)
1.99 + else:
1.100 + return XMPPClientElement(_node, self, context_node.ownerDocument)
1.101 +
1.102 + # Make special registration elements.
1.103 +
1.104 + elif Node_namespaceURI(_node) == XMPP_REGISTER_NAMESPACE:
1.105 + return XMPPRegisterElement(_node, self, context_node.ownerDocument)
1.106 +
1.107 + # Make special authentication elements.
1.108 +
1.109 + elif Node_namespaceURI(_node) == XMPP_SASL_NAMESPACE:
1.110 + if Node_localName(_node) == "auth":
1.111 + return XMPPAuthElement(_node, self, context_node.ownerDocument)
1.112 +
1.113 + # Make special stream elements.
1.114 +
1.115 + elif Node_namespaceURI(_node) == XMPP_STREAMS_NAMESPACE:
1.116 + if Node_localName(_node) == "stream":
1.117 + return XMPPStreamElement(_node, self, context_node.ownerDocument)
1.118 +
1.119 + # Otherwise, make generic XMPP elements.
1.120 +
1.121 + return XMPPElement(_node, self, context_node.ownerDocument)
1.122 +
1.123 + else:
1.124 + return libxml2dom.Implementation.get_node(self, _node, context_node)
1.125 +
1.126 + # Convenience functions.
1.127 +
1.128 + def createXMPPStanza(self, namespaceURI, localName):
1.129 +
1.130 + "Create a new XMPP stanza document (fragment)."
1.131 +
1.132 + return XMPPDocument(Node_createDocument(namespaceURI, localName, None), self).documentElement
1.133 +
1.134 +# Node classes.
1.135 +
1.136 +class XMPPNode(libxml2dom.Node):
1.137 +
1.138 + "Convenience modifications to nodes specific to libxml2dom.svg."
1.139 +
1.140 + def xpath(self, expr, variables=None, namespaces=None):
1.141 +
1.142 + """
1.143 + Evaluate the given 'expr' using the optional 'variables' and
1.144 + 'namespaces'. If not otherwise specified, the "stream" prefix will be
1.145 + bound to XMPP_STREAMS_NAMESPACE as defined in this module.
1.146 + """
1.147 +
1.148 + ns = {
1.149 + "bind" : XMPP_BIND_NAMESPACE,
1.150 + "client" : XMPP_CLIENT_NAMESPACE,
1.151 + "register" : XMPP_REGISTER_NAMESPACE,
1.152 + "sasl" : XMPP_SASL_NAMESPACE,
1.153 + "session" : XMPP_SESSION_NAMESPACE,
1.154 + "stream" : XMPP_STREAMS_NAMESPACE
1.155 + }
1.156 + ns.update(namespaces or {})
1.157 + return libxml2dom.Node.xpath(self, expr, variables, ns)
1.158 +
1.159 +class XMPPDocument(libxml2dom._Document, XMPPNode):
1.160 +
1.161 + "An XMPP document fragment."
1.162 +
1.163 + pass
1.164 +
1.165 +class XMPPAuthElement(XMPPNode):
1.166 +
1.167 + "An XMPP auth element."
1.168 +
1.169 + def _mechanism(self):
1.170 + return self.getAttribute("mechanism")
1.171 +
1.172 + def _setMechanism(self, value):
1.173 + self.setAttribute("mechanism", value)
1.174 +
1.175 + def _value(self):
1.176 + return self.textContent
1.177 +
1.178 + def setCredentials(self, jid, username, password):
1.179 +
1.180 + # NOTE: This is what xmpppy does. Beware of the leopard, with respect to
1.181 + # NOTE: the specifications.
1.182 +
1.183 + b64value = base64.encodestring("%s\x00%s\x00%s" % (jid, username, password))
1.184 + text = self.ownerDocument.createTextNode(b64value)
1.185 + self.appendChild(text)
1.186 +
1.187 +
1.188 + mechanism = property(_mechanism, _setMechanism)
1.189 + value = property(_value)
1.190 +
1.191 +class XMPPBindElement(XMPPNode):
1.192 +
1.193 + "An XMPP bind element."
1.194 +
1.195 + def _resource(self):
1.196 + return "".join(self.xpath("resource/text()"))
1.197 +
1.198 + def _setResource(self, value):
1.199 + resources = self.xpath("resource")
1.200 + for resource in resources:
1.201 + self.removeChild(resource)
1.202 + resource = self.ownerDocument.createElement("resource")
1.203 + self.appendChild(resource)
1.204 + text = self.ownerDocument.createTextNode(value)
1.205 + resource.appendChild(text)
1.206 +
1.207 + resource = property(_resource, _setResource)
1.208 +
1.209 +class XMPPClientElement(XMPPNode):
1.210 +
1.211 + "An XMPP client element."
1.212 +
1.213 + def _id(self):
1.214 + return self.getAttribute("id")
1.215 +
1.216 + def _setId(self, value):
1.217 + self.setAttribute("id", value)
1.218 +
1.219 + def _delId(self):
1.220 + self.removeAttribute("id")
1.221 +
1.222 + def _from(self):
1.223 + return self.getAttribute("from")
1.224 +
1.225 + def _setFrom(self, value):
1.226 + self.setAttribute("from", value)
1.227 +
1.228 + def _delFrom(self):
1.229 + self.removeAttribute("from")
1.230 +
1.231 + def _to(self):
1.232 + return self.getAttribute("to")
1.233 +
1.234 + def _setTo(self, value):
1.235 + self.setAttribute("to", value)
1.236 +
1.237 + def _delTo(self):
1.238 + self.removeAttribute("to")
1.239 +
1.240 + def _type(self):
1.241 + return self.getAttribute("type")
1.242 +
1.243 + def _setType(self, value):
1.244 + self.setAttribute("type", value)
1.245 +
1.246 + def _delType(self):
1.247 + self.removeAttribute("type")
1.248 +
1.249 + id = property(_id, _setId, _delId)
1.250 + from_ = property(_from, _setFrom, _delFrom)
1.251 + to = property(_to, _setTo, _delTo)
1.252 + type = property(_type, _setType, _delType)
1.253 +
1.254 +class XMPPIqElement(XMPPClientElement):
1.255 +
1.256 + """
1.257 + An XMPP 'iq' element used in instant messaging and registration.
1.258 + See: http://www.xmpp.org/rfcs/rfc3921.html
1.259 + See: http://www.xmpp.org/extensions/xep-0077.html
1.260 + """
1.261 +
1.262 + def _bind(self):
1.263 + return (self.xpath("bind:bind") or [None])[0]
1.264 +
1.265 + def _query(self):
1.266 + return (self.xpath("register:query") or [None])[0]
1.267 +
1.268 + def _session(self):
1.269 + return (self.xpath("session:session") or [None])[0]
1.270 +
1.271 + bind = property(_bind)
1.272 + query = property(_query)
1.273 + session = property(_session)
1.274 +
1.275 + def createBind(self):
1.276 + return self.ownerDocument.createElementNS(XMPP_BIND_NAMESPACE, "bind")
1.277 +
1.278 + def createQuery(self):
1.279 + return self.ownerDocument.createElementNS(XMPP_REGISTER_NAMESPACE, "query")
1.280 +
1.281 + def createSession(self):
1.282 + return self.ownerDocument.createElementNS(XMPP_SESSION_NAMESPACE, "session")
1.283 +
1.284 + def makeBind(self):
1.285 + bind = self.createBind()
1.286 + self.appendChild(bind)
1.287 + self.id = "bind1"
1.288 + self.type = "set"
1.289 +
1.290 + def makeQuery(self):
1.291 + query = self.createQuery()
1.292 + self.appendChild(query)
1.293 + self.id = "register1"
1.294 + self.type = "get"
1.295 +
1.296 + def makeRegistration(self):
1.297 + self.id = "register2"
1.298 + self.type = "set"
1.299 +
1.300 + def makeSession(self, host):
1.301 + session = self.createSession()
1.302 + self.appendChild(session)
1.303 + self.id = "session1"
1.304 + self.type = "set"
1.305 + self.to = host
1.306 +
1.307 +class XMPPRegisterElement(XMPPNode):
1.308 +
1.309 + """
1.310 + A registration element.
1.311 + See: http://www.xmpp.org/extensions/xep-0077.html
1.312 + """
1.313 +
1.314 + def __setitem__(self, name, value):
1.315 + element = self.ownerDocument.createElement(name)
1.316 + text = self.ownerDocument.createTextNode(value)
1.317 + element = self.appendChild(element)
1.318 + element.appendChild(text)
1.319 +
1.320 +class XMPPStreamElement(XMPPNode):
1.321 + pass
1.322 +
1.323 +class XMPPElement(XMPPNode):
1.324 + pass
1.325 +
1.326 +# Classes providing XMPP session support.
1.327 +
1.328 +class SessionTerminated(Exception):
1.329 + pass
1.330 +
1.331 +class Session:
1.332 +
1.333 + "An XMPP session."
1.334 +
1.335 + connect_str = """\
1.336 +<?xml version="1.0"?>
1.337 +<stream:stream to='%s' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>"""
1.338 +
1.339 + disconnect_str = """\
1.340 +</stream:stream>"""
1.341 +
1.342 + def __init__(self, address, timeout=500, bufsize=1024, encoding="utf-8"):
1.343 +
1.344 + """
1.345 + Initialise an XMPP session using the given 'address': a tuple of the
1.346 + form (hostname, port). The optional 'timeout' (in milliseconds) is used
1.347 + for polling the connection for new data, and the optional 'encoding'
1.348 + specifies the character encoding employed in the communications.
1.349 + """
1.350 +
1.351 + self.timeout = timeout
1.352 + self.bufsize = bufsize
1.353 + self.encoding = encoding
1.354 + self.poller = select.poll()
1.355 + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1.356 + self.socket.setblocking(1)
1.357 + self.socket.connect(address)
1.358 + self.poller.register(self.socket.fileno(), select.POLLIN | select.POLLHUP | select.POLLNVAL | select.POLLERR)
1.359 +
1.360 + def _ready(self, timeout):
1.361 +
1.362 + """
1.363 + Return whether data can be read from the server, waiting as long as the
1.364 + specified 'timeout' (forever if set to None).
1.365 + """
1.366 +
1.367 + return self.poller.poll(timeout)
1.368 +
1.369 + def read(self):
1.370 +
1.371 + "Read as much as possible from the server."
1.372 +
1.373 + l = []
1.374 + fds = self._ready(self.timeout)
1.375 + try:
1.376 + while fds:
1.377 + for fd, status in fds:
1.378 + if fd == self.socket.fileno():
1.379 + if status & (select.POLLHUP | select.POLLNVAL | select.POLLERR):
1.380 + raise SessionTerminated
1.381 + if status & select.POLLIN:
1.382 + c = self.socket.recv(self.bufsize)
1.383 + if c:
1.384 + l.append(c)
1.385 + else:
1.386 + raise SessionTerminated
1.387 + fds = self.poller.poll(self.timeout)
1.388 + except SessionTerminated:
1.389 + pass
1.390 + return "".join(l)
1.391 +
1.392 + def write(self, s):
1.393 +
1.394 + "Write the plain string 's' to the server."
1.395 +
1.396 + self.socket.send(s)
1.397 +
1.398 + def send(self, stanza):
1.399 +
1.400 + """
1.401 + Send the 'stanza' to the server, returning a response stanza if an
1.402 + immediate response was provided, or None otherwise.
1.403 + """
1.404 +
1.405 + stanza.toStream(self, encoding=self.encoding)
1.406 + return self._receive()
1.407 +
1.408 + def _receive(self):
1.409 +
1.410 + """
1.411 + Return either a stanza or None, depending on whether anything was read
1.412 + from the server.
1.413 + """
1.414 +
1.415 + s = self.read()
1.416 + if s:
1.417 + return parseString(s).documentElement
1.418 + else:
1.419 + return None
1.420 +
1.421 + def receive(self, timeout):
1.422 +
1.423 + """
1.424 + Wait for an incoming stanza, or as long as 'timeout' (in milliseconds),
1.425 + returning either a stanza document (fragment) or None if nothing was
1.426 + received.
1.427 + """
1.428 +
1.429 + if self._ready(timeout):
1.430 + return self._receive()
1.431 + else:
1.432 + return None
1.433 +
1.434 + # Stanza creation.
1.435 +
1.436 + def createAuth(self):
1.437 + return self.createStanza(XMPP_SASL_NAMESPACE, "auth")
1.438 +
1.439 + def createIq(self):
1.440 + return self.createStanza(XMPP_CLIENT_NAMESPACE, "iq")
1.441 +
1.442 + def createMessage(self):
1.443 + return self.createStanza(XMPP_CLIENT_NAMESPACE, "message")
1.444 +
1.445 + def createStanza(self, namespaceURI, localName):
1.446 + return createXMPPStanza(namespaceURI, localName)
1.447 +
1.448 + # High-level methods.
1.449 +
1.450 + def connect(self, host):
1.451 +
1.452 + # NOTE: Nasty sending of the raw text because it involves only a start
1.453 + # NOTE: tag.
1.454 +
1.455 + self.write(self.connect_str % host)
1.456 + return parseString(self.read(), unfinished=1).documentElement
1.457 +
1.458 +# Utility functions.
1.459 +
1.460 +createDocument = libxml2dom.createDocument
1.461 +createDocumentType = libxml2dom.createDocumentType
1.462 +
1.463 +def createXMPPStanza(namespaceURI, localName):
1.464 + return default_impl.createXMPPStanza(namespaceURI, localName)
1.465 +
1.466 +def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None):
1.467 + return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl))
1.468 +
1.469 +def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None):
1.470 + return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl))
1.471 +
1.472 +def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None):
1.473 + return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl))
1.474 +
1.475 +def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None):
1.476 + return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl))
1.477 +
1.478 +# Single instance of the implementation.
1.479 +
1.480 +default_impl = XMPPImplementation()
1.481 +
1.482 +# vim: tabstop=4 expandtab shiftwidth=4