1 #!/usr/bin/env python 2 3 """ 4 XMPP support using libxml2dom to capture stanzas as documents. The XMPP 5 specification employs an "open" or unfinished document as the basis for 6 communications between client and server - this presents problems for 7 DOM-oriented libraries. 8 9 Various Internet standards specifications exist for XMPP. 10 See: http://www.xmpp.org/rfcs/rfc3920.html 11 See: http://www.xmpp.org/rfcs/rfc3921.html 12 13 Copyright (C) 2007, 2009 Paul Boddie <paul@boddie.org.uk> 14 15 This program is free software; you can redistribute it and/or modify it under 16 the terms of the GNU Lesser General Public License as published by the Free 17 Software Foundation; either version 3 of the License, or (at your option) any 18 later version. 19 20 This program is distributed in the hope that it will be useful, but WITHOUT 21 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 22 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 23 details. 24 25 You should have received a copy of the GNU Lesser General Public License along 26 with this program. If not, see <http://www.gnu.org/licenses/>. 27 28 -------- 29 30 The process of connecting, authenticating, and so on is quite convoluted: 31 32 s = libxml2dom.xmpp.Session(("localhost", 5222)) 33 d = s.connect("host") 34 auth = s.createAuth() # provides access to the stanza 35 auth.mechanism = "PLAIN" # choose a supported mechanism 36 auth.setCredentials(jid, username, password) # for PLAIN authentication only 37 s.send(auth) # send the authentication request 38 d = s.receive() # hopefully a success response 39 d = s.connect("host") # have to reconnect! 40 iq = s.createIq() # make an 'iq' stanza 41 iq.makeBind() # set up a binding operation 42 s.send(iq) # send the binding request 43 d = s.receive() # hopefully a success response 44 iq = s.createIq() # make an 'iq' stanza 45 iq.makeSession() # set up a session 46 s.send(iq) # send the session request 47 48 See tests/xmpp_test.py for more details. 49 """ 50 51 import libxml2dom 52 from libxml2dom.macrolib import * 53 from libxml2dom.macrolib import \ 54 createDocument as Node_createDocument 55 import socket 56 import select 57 import base64 # for auth elements 58 59 # XMPP-related namespaces. 60 61 XMPP_BIND_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-bind" 62 XMPP_CLIENT_NAMESPACE = "jabber:client" 63 XEP_0022_EVENT_NAMESPACE = "jabber:x:event" 64 XMPP_REGISTER_NAMESPACE = "jabber:iq:register" 65 XMPP_SASL_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl" 66 XMPP_SESSION_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-session" 67 XMPP_STREAMS_NAMESPACE = "http://etherx.jabber.org/streams" 68 69 # Default namespace bindings for XPath. 70 71 default_ns = { 72 "bind" : XMPP_BIND_NAMESPACE, 73 "client" : XMPP_CLIENT_NAMESPACE, 74 "event": XEP_0022_EVENT_NAMESPACE, 75 "register" : XMPP_REGISTER_NAMESPACE, 76 "sasl" : XMPP_SASL_NAMESPACE, 77 "session" : XMPP_SESSION_NAMESPACE, 78 "stream" : XMPP_STREAMS_NAMESPACE 79 } 80 81 class XMPPImplementation(libxml2dom.Implementation): 82 83 "Contains an XMPP-specific implementation." 84 85 # Wrapping of documents. 86 87 def adoptDocument(self, node): 88 return XMPPDocument(node, self) 89 90 # Factory functions. 91 92 def get_node(self, _node, context_node): 93 94 """ 95 Get a libxml2dom node for the given low-level '_node' and libxml2dom 96 'context_node'. 97 """ 98 99 if Node_nodeType(_node) == context_node.ELEMENT_NODE: 100 101 # Make special binding elements. 102 103 if Node_namespaceURI(_node) == XMPP_BIND_NAMESPACE: 104 if Node_localName(_node) == "bind": 105 return XMPPBindElement(_node, self, context_node.ownerDocument) 106 107 # Make special client elements. 108 109 elif Node_namespaceURI(_node) == XMPP_CLIENT_NAMESPACE: 110 if Node_localName(_node) == "iq": 111 return XMPPIqElement(_node, self, context_node.ownerDocument) 112 elif Node_localName(_node) == "message": 113 return XMPPMessageElement(_node, self, context_node.ownerDocument) 114 elif Node_localName(_node) == "presence": 115 return XMPPPresenceElement(_node, self, context_node.ownerDocument) 116 else: 117 return XMPPClientElement(_node, self, context_node.ownerDocument) 118 119 # Make special event elements. 120 121 elif Node_namespaceURI(_node) == XEP_0022_EVENT_NAMESPACE: 122 return XEP0022EventElement(_node, self, context_node.ownerDocument) 123 124 # Make special registration elements. 125 126 elif Node_namespaceURI(_node) == XMPP_REGISTER_NAMESPACE: 127 return XMPPRegisterElement(_node, self, context_node.ownerDocument) 128 129 # Make special authentication elements. 130 131 elif Node_namespaceURI(_node) == XMPP_SASL_NAMESPACE: 132 if Node_localName(_node) == "auth": 133 return XMPPAuthElement(_node, self, context_node.ownerDocument) 134 elif Node_localName(_node) == "failure": 135 return XMPPFailureElement(_node, self, context_node.ownerDocument) 136 137 # Make special stream elements. 138 139 elif Node_namespaceURI(_node) == XMPP_STREAMS_NAMESPACE: 140 if Node_localName(_node) == "stream": 141 return XMPPStreamElement(_node, self, context_node.ownerDocument) 142 143 # Otherwise, make generic XMPP elements. 144 145 return XMPPElement(_node, self, context_node.ownerDocument) 146 147 else: 148 return libxml2dom.Implementation.get_node(self, _node, context_node) 149 150 # Convenience functions. 151 152 def createXMPPStanza(self, namespaceURI, localName): 153 154 "Create a new XMPP stanza document (fragment)." 155 156 return XMPPDocument(Node_createDocument(namespaceURI, localName, None), self).documentElement 157 158 # Node classes. 159 160 class XMPPNode(libxml2dom.Node): 161 162 "Convenience modifications to nodes specific to libxml2dom.xmpp." 163 164 def xpath(self, expr, variables=None, namespaces=None): 165 166 """ 167 Evaluate the given 'expr' using the optional 'variables' and 168 'namespaces'. If not otherwise specified, the prefixes given in the 169 module global 'default_ns' will be bound as in that dictionary. 170 """ 171 172 ns = {} 173 ns.update(default_ns) 174 ns.update(namespaces or {}) 175 return libxml2dom.Node.xpath(self, expr, variables, ns) 176 177 class XMPPDocument(libxml2dom._Document, XMPPNode): 178 179 "An XMPP document fragment." 180 181 pass 182 183 class XMPPElement(XMPPNode): 184 pass 185 186 class XMPPAuthElement(XMPPNode): 187 188 "An XMPP auth element." 189 190 def _mechanism(self): 191 return self.getAttribute("mechanism") 192 193 def _setMechanism(self, value): 194 self.setAttribute("mechanism", value) 195 196 def _value(self): 197 return self.textContent 198 199 def setCredentials(self, jid, username, password): 200 201 # NOTE: This is what xmpppy does. Beware of the leopard, with respect to 202 # NOTE: the specifications. 203 204 b64value = base64.encodestring("%s\x00%s\x00%s" % (jid, username, password)) 205 text = self.ownerDocument.createTextNode(b64value.strip()) 206 self.appendChild(text) 207 208 mechanism = property(_mechanism, _setMechanism) 209 value = property(_value) 210 211 class XMPPBindElement(XMPPNode): 212 213 "An XMPP bind element." 214 215 def _resource(self): 216 return "".join(self.xpath("resource/text()")) 217 218 def _setResource(self, value): 219 resources = self.xpath("resource") 220 for resource in resources: 221 self.removeChild(resource) 222 resource = self.ownerDocument.createElement("resource") 223 self.appendChild(resource) 224 text = self.ownerDocument.createTextNode(value) 225 resource.appendChild(text) 226 227 resource = property(_resource, _setResource) 228 229 class XMPPClientElement(XMPPNode): 230 231 "An XMPP client element." 232 233 def _id(self): 234 return self.getAttribute("id") 235 236 def _setId(self, value): 237 self.setAttribute("id", value) 238 239 def _delId(self): 240 self.removeAttribute("id") 241 242 def _from(self): 243 return self.getAttribute("from") 244 245 def _setFrom(self, value): 246 self.setAttribute("from", value) 247 248 def _delFrom(self): 249 self.removeAttribute("from") 250 251 def _to(self): 252 return self.getAttribute("to") 253 254 def _setTo(self, value): 255 self.setAttribute("to", value) 256 257 def _delTo(self): 258 self.removeAttribute("to") 259 260 def _type(self): 261 return self.getAttribute("type") 262 263 def _setType(self, value): 264 self.setAttribute("type", value) 265 266 def _delType(self): 267 self.removeAttribute("type") 268 269 id = property(_id, _setId, _delId) 270 from_ = property(_from, _setFrom, _delFrom) 271 to = property(_to, _setTo, _delTo) 272 type = property(_type, _setType, _delType) 273 274 class XMPPFailureElement(XMPPElement): 275 276 "An XMPP failure element." 277 278 def _reason(self): 279 return self.xpath("*")[0].localName 280 281 def _setReason(self, reason_text): 282 for reason in self.xpath("*"): 283 self.removeChild(reason) 284 element = self.ownerDocument.createElement(reason_text) 285 self.appendChild(element) 286 287 reason = property(_reason, _setReason) 288 289 class XMPPMessageElement(XMPPClientElement): 290 291 "An XMPP message element." 292 293 def _event(self): 294 return self.xpath(".//event:*")[0] 295 296 def _body(self): 297 return self.xpath("./client:body")[0] 298 299 def _setBody(self, body): 300 self.appendChild(body) 301 302 def _delBody(self): 303 self.removeChild(self.body) 304 305 def createBody(self): 306 return self.ownerDocument.createElementNS(XMPP_CLIENT_NAMESPACE, "body") 307 308 body = property(_body, _setBody, _delBody) 309 event = property(_event) 310 311 class XEP0022EventElement(XMPPNode): 312 313 "An XEP-0022 event element." 314 315 def _offline(self): 316 return bool(self.xpath("./event:offline")) 317 318 def _delivered(self): 319 return bool(self.xpath("./event:delivered")) 320 321 def _displayed(self): 322 return bool(self.xpath("./event:displayed")) 323 324 def _composing(self): 325 return bool(self.xpath("./event:composing")) 326 327 def _id(self): 328 ids = self.xpath("./event:id") 329 if ids: 330 return ids[0].textContent 331 else: 332 return None 333 334 offline = property(_offline) 335 delivered = property(_delivered) 336 displayed = property(_displayed) 337 composing = property(_composing) 338 id = property(_id) 339 340 class XMPPPresenceElement(XMPPClientElement): 341 342 "An XMPP presence element." 343 344 pass 345 346 class XMPPIqElement(XMPPClientElement): 347 348 """ 349 An XMPP 'iq' element used in instant messaging and registration. 350 See: http://www.xmpp.org/rfcs/rfc3921.html 351 See: http://www.xmpp.org/extensions/xep-0077.html 352 """ 353 354 def _bind(self): 355 return (self.xpath("bind:bind") or [None])[0] 356 357 def _query(self): 358 return (self.xpath("register:query") or [None])[0] 359 360 def _session(self): 361 return (self.xpath("session:session") or [None])[0] 362 363 bind = property(_bind) 364 registration = query = property(_query) 365 session = property(_session) 366 367 def createBind(self): 368 return self.ownerDocument.createElementNS(XMPP_BIND_NAMESPACE, "bind") 369 370 def createQuery(self): 371 return self.ownerDocument.createElementNS(XMPP_REGISTER_NAMESPACE, "query") 372 373 def createSession(self): 374 return self.ownerDocument.createElementNS(XMPP_SESSION_NAMESPACE, "session") 375 376 def makeBind(self): 377 bind = self.createBind() 378 self.appendChild(bind) 379 self.id = "bind1" 380 self.type = "set" 381 382 def makeQuery(self): 383 query = self.createQuery() 384 self.appendChild(query) 385 self.id = "register1" 386 self.type = "get" 387 388 def makeRegistration(self): 389 query = self.createQuery() 390 self.appendChild(query) 391 self.id = "register2" 392 self.type = "set" 393 394 def makeUnregistration(self): 395 query = self.createQuery() 396 self.appendChild(query) 397 query.appendChild(self.ownerDocument.createElement("remove")) 398 self.id = "unreg1" 399 self.type = "set" 400 401 def makeSession(self, host): 402 session = self.createSession() 403 self.appendChild(session) 404 self.id = "session1" 405 self.type = "set" 406 self.to = host 407 408 class XMPPRegisterElement(XMPPNode): 409 410 """ 411 A registration element. 412 See: http://www.xmpp.org/extensions/xep-0077.html 413 """ 414 415 def __setitem__(self, name, value): 416 element = self.ownerDocument.createElement(name) 417 text = self.ownerDocument.createTextNode(value) 418 element = self.appendChild(element) 419 element.appendChild(text) 420 421 class XMPPStreamElement(XMPPNode): 422 pass 423 424 # Classes providing XMPP session support. 425 426 class SessionTerminated(Exception): 427 pass 428 429 class Session: 430 431 "An XMPP session." 432 433 connect_str = """\ 434 <?xml version="1.0"?> 435 <stream:stream to='%s' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>""" 436 437 disconnect_str = """\ 438 </stream:stream>""" 439 440 def __init__(self, address, bufsize=1024, encoding="utf-8"): 441 442 """ 443 Initialise an XMPP session using the given 'address': a tuple of the 444 form (hostname, port). The optional 'encoding' specifies the character 445 encoding employed in the communications. 446 """ 447 448 self.bufsize = bufsize 449 self.encoding = encoding 450 self.poller = select.poll() 451 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 452 self.socket.setblocking(1) 453 self.socket.connect(address) 454 self.poller.register(self.socket.fileno(), select.POLLIN | select.POLLHUP | select.POLLNVAL | select.POLLERR) 455 456 def read(self, timeout=None): 457 458 """ 459 Read as much as possible from the server, waiting as long as the 460 specified 'timeout' (forever if set to None) for a message to arrive. 461 """ 462 463 context = Parser_push() 464 Parser_configure(context) 465 466 have_read = 0 467 fds = self.poller.poll(timeout) 468 469 for fd, status in fds: 470 if fd == self.socket.fileno(): 471 if status & (select.POLLHUP | select.POLLNVAL | select.POLLERR): 472 raise SessionTerminated 473 if status & select.POLLIN: 474 have_read = 1 475 c = self.socket.recv(self.bufsize) 476 Parser_feed(context, c) 477 if Parser_well_formed(context): 478 return default_impl.adoptDocument(Parser_document(context)) 479 480 if have_read: 481 return default_impl.adoptDocument(Parser_document(context)) 482 else: 483 return None 484 485 def write(self, s): 486 487 "Write the plain string 's' to the server." 488 489 self.socket.send(s) 490 491 def send(self, stanza): 492 493 """ 494 Send the 'stanza' to the server. 495 """ 496 497 stanza.toStream(self, encoding=self.encoding) 498 499 def receive(self, timeout=None): 500 501 """ 502 Wait for an incoming stanza, or as long as 'timeout' (in milliseconds), 503 or forever if 'timeout' is omitted or set to None, returning either a 504 stanza document (fragment) or None if nothing was received. 505 """ 506 507 doc = self.read(timeout) 508 if doc is None: 509 return None 510 else: 511 stanza = doc.documentElement 512 513 # Add implied namespace (from the outermost element). 514 # NOTE: This should possibly use the real namespace details from the 515 # NOTE: stream element. 516 517 if stanza.namespaceURI is None: 518 new_stanza = self.createStanza(XMPP_CLIENT_NAMESPACE, stanza.name) 519 new_doc = new_stanza.ownerDocument 520 for attribute in stanza.attributes: 521 new_stanza.attributes.setNamedItemNS(attribute) 522 for child in stanza.childNodes: 523 n = new_doc.importNode(child, 1) 524 new_stanza.appendChild(n) 525 stanza = new_stanza 526 527 return stanza 528 529 # Stanza creation. 530 531 def createAuth(self): 532 return self.createStanza(XMPP_SASL_NAMESPACE, "auth") 533 534 def createIq(self): 535 return self.createStanza(XMPP_CLIENT_NAMESPACE, "iq") 536 537 def createMessage(self): 538 return self.createStanza(XMPP_CLIENT_NAMESPACE, "message") 539 540 def createPresence(self): 541 return self.createStanza(XMPP_CLIENT_NAMESPACE, "presence") 542 543 def createStanza(self, namespaceURI, localName): 544 return createXMPPStanza(namespaceURI, localName) 545 546 # High-level methods. 547 548 def connect(self, host): 549 550 # NOTE: Nasty sending of the raw text because it involves only a start 551 # NOTE: tag. 552 553 self.write(self.connect_str % host) 554 return self.receive() 555 556 def disconnect(self): 557 558 # NOTE: Nasty sending of the raw text because it involves only an end 559 # NOTE: tag. 560 561 self.write(self.disconnect_str) 562 563 # Utility functions. 564 565 createDocument = libxml2dom.createDocument 566 createDocumentType = libxml2dom.createDocumentType 567 568 def createXMPPStanza(namespaceURI, localName): 569 return default_impl.createXMPPStanza(namespaceURI, localName) 570 571 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 572 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 573 574 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 575 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 576 577 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 578 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 579 580 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 581 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 582 583 # Single instance of the implementation. 584 585 default_impl = XMPPImplementation() 586 587 # vim: tabstop=4 expandtab shiftwidth=4