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, 2014 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 "An XMPP-specific node." 163 164 pass 165 166 class XMPPDocument(libxml2dom._Document, XMPPNode): 167 168 "An XMPP document fragment." 169 170 def __init__(self, node, impl, namespaces=None): 171 172 """ 173 Initialise the document with the given 'node', implementation 'impl', 174 and 'namespaces' details. 175 """ 176 177 libxml2dom._Document.__init__(self, node, impl, None) 178 self._update_namespaces([default_ns, namespaces]) 179 180 class XMPPElement(XMPPNode): 181 pass 182 183 class XMPPAuthElement(XMPPNode): 184 185 "An XMPP auth element." 186 187 def _mechanism(self): 188 return self.getAttribute("mechanism") 189 190 def _setMechanism(self, value): 191 self.setAttribute("mechanism", value) 192 193 def _value(self): 194 return self.textContent 195 196 def setCredentials(self, jid, username, password): 197 198 # NOTE: This is what xmpppy does. Beware of the leopard, with respect to 199 # NOTE: the specifications. 200 201 b64value = base64.encodestring("%s\x00%s\x00%s" % (jid, username, password)) 202 text = self.ownerDocument.createTextNode(b64value.strip()) 203 self.appendChild(text) 204 205 mechanism = property(_mechanism, _setMechanism) 206 value = property(_value) 207 208 class XMPPBindElement(XMPPNode): 209 210 "An XMPP bind element." 211 212 def _resource(self): 213 return "".join(self.xpath("resource/text()")) 214 215 def _setResource(self, value): 216 resources = self.xpath("resource") 217 for resource in resources: 218 self.removeChild(resource) 219 resource = self.ownerDocument.createElement("resource") 220 self.appendChild(resource) 221 text = self.ownerDocument.createTextNode(value) 222 resource.appendChild(text) 223 224 resource = property(_resource, _setResource) 225 226 class XMPPClientElement(XMPPNode): 227 228 "An XMPP client element." 229 230 def _id(self): 231 return self.getAttribute("id") 232 233 def _setId(self, value): 234 self.setAttribute("id", value) 235 236 def _delId(self): 237 self.removeAttribute("id") 238 239 def _from(self): 240 return self.getAttribute("from") 241 242 def _setFrom(self, value): 243 self.setAttribute("from", value) 244 245 def _delFrom(self): 246 self.removeAttribute("from") 247 248 def _to(self): 249 return self.getAttribute("to") 250 251 def _setTo(self, value): 252 self.setAttribute("to", value) 253 254 def _delTo(self): 255 self.removeAttribute("to") 256 257 def _type(self): 258 return self.getAttribute("type") 259 260 def _setType(self, value): 261 self.setAttribute("type", value) 262 263 def _delType(self): 264 self.removeAttribute("type") 265 266 id = property(_id, _setId, _delId) 267 from_ = property(_from, _setFrom, _delFrom) 268 to = property(_to, _setTo, _delTo) 269 type = property(_type, _setType, _delType) 270 271 class XMPPFailureElement(XMPPElement): 272 273 "An XMPP failure element." 274 275 def _reason(self): 276 return self.xpath("*")[0].localName 277 278 def _setReason(self, reason_text): 279 for reason in self.xpath("*"): 280 self.removeChild(reason) 281 element = self.ownerDocument.createElement(reason_text) 282 self.appendChild(element) 283 284 reason = property(_reason, _setReason) 285 286 class XMPPMessageElement(XMPPClientElement): 287 288 "An XMPP message element." 289 290 def _event(self): 291 return self.xpath(".//event:*")[0] 292 293 def _body(self): 294 return self.xpath("./client:body")[0] 295 296 def _setBody(self, body): 297 self.appendChild(body) 298 299 def _delBody(self): 300 self.removeChild(self.body) 301 302 def createBody(self): 303 return self.ownerDocument.createElementNS(XMPP_CLIENT_NAMESPACE, "body") 304 305 body = property(_body, _setBody, _delBody) 306 event = property(_event) 307 308 class XEP0022EventElement(XMPPNode): 309 310 "An XEP-0022 event element." 311 312 def _offline(self): 313 return bool(self.xpath("./event:offline")) 314 315 def _delivered(self): 316 return bool(self.xpath("./event:delivered")) 317 318 def _displayed(self): 319 return bool(self.xpath("./event:displayed")) 320 321 def _composing(self): 322 return bool(self.xpath("./event:composing")) 323 324 def _id(self): 325 ids = self.xpath("./event:id") 326 if ids: 327 return ids[0].textContent 328 else: 329 return None 330 331 offline = property(_offline) 332 delivered = property(_delivered) 333 displayed = property(_displayed) 334 composing = property(_composing) 335 id = property(_id) 336 337 class XMPPPresenceElement(XMPPClientElement): 338 339 "An XMPP presence element." 340 341 pass 342 343 class XMPPIqElement(XMPPClientElement): 344 345 """ 346 An XMPP 'iq' element used in instant messaging and registration. 347 See: http://www.xmpp.org/rfcs/rfc3921.html 348 See: http://www.xmpp.org/extensions/xep-0077.html 349 """ 350 351 def _bind(self): 352 return (self.xpath("bind:bind") or [None])[0] 353 354 def _query(self): 355 return (self.xpath("register:query") or [None])[0] 356 357 def _session(self): 358 return (self.xpath("session:session") or [None])[0] 359 360 bind = property(_bind) 361 registration = query = property(_query) 362 session = property(_session) 363 364 def createBind(self): 365 return self.ownerDocument.createElementNS(XMPP_BIND_NAMESPACE, "bind") 366 367 def createQuery(self): 368 return self.ownerDocument.createElementNS(XMPP_REGISTER_NAMESPACE, "query") 369 370 def createSession(self): 371 return self.ownerDocument.createElementNS(XMPP_SESSION_NAMESPACE, "session") 372 373 def makeBind(self): 374 bind = self.createBind() 375 self.appendChild(bind) 376 self.id = "bind1" 377 self.type = "set" 378 379 def makeQuery(self): 380 query = self.createQuery() 381 self.appendChild(query) 382 self.id = "register1" 383 self.type = "get" 384 385 def makeRegistration(self): 386 query = self.createQuery() 387 self.appendChild(query) 388 self.id = "register2" 389 self.type = "set" 390 391 def makeUnregistration(self): 392 query = self.createQuery() 393 self.appendChild(query) 394 query.appendChild(self.ownerDocument.createElement("remove")) 395 self.id = "unreg1" 396 self.type = "set" 397 398 def makeSession(self, host): 399 session = self.createSession() 400 self.appendChild(session) 401 self.id = "session1" 402 self.type = "set" 403 self.to = host 404 405 class XMPPRegisterElement(XMPPNode): 406 407 """ 408 A registration element. 409 See: http://www.xmpp.org/extensions/xep-0077.html 410 """ 411 412 def __setitem__(self, name, value): 413 element = self.ownerDocument.createElement(name) 414 text = self.ownerDocument.createTextNode(value) 415 element = self.appendChild(element) 416 element.appendChild(text) 417 418 class XMPPStreamElement(XMPPNode): 419 pass 420 421 # Classes providing XMPP session support. 422 423 class SessionTerminated(Exception): 424 pass 425 426 class Session: 427 428 "An XMPP session." 429 430 connect_str = """\ 431 <?xml version="1.0"?> 432 <stream:stream to='%s' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>""" 433 434 disconnect_str = """\ 435 </stream:stream>""" 436 437 def __init__(self, address, bufsize=1024, encoding="utf-8"): 438 439 """ 440 Initialise an XMPP session using the given 'address': a tuple of the 441 form (hostname, port). The optional 'encoding' specifies the character 442 encoding employed in the communications. 443 """ 444 445 self.bufsize = bufsize 446 self.encoding = encoding 447 self.poller = select.poll() 448 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 449 self.socket.setblocking(1) 450 self.socket.connect(address) 451 self.poller.register(self.socket.fileno(), select.POLLIN | select.POLLHUP | select.POLLNVAL | select.POLLERR) 452 453 def read(self, timeout=None): 454 455 """ 456 Read as much as possible from the server, waiting as long as the 457 specified 'timeout' (forever if set to None) for a message to arrive. 458 """ 459 460 context = Parser_push() 461 Parser_configure(context) 462 463 have_read = 0 464 fds = self.poller.poll(timeout) 465 466 for fd, status in fds: 467 if fd == self.socket.fileno(): 468 if status & (select.POLLHUP | select.POLLNVAL | select.POLLERR): 469 raise SessionTerminated 470 if status & select.POLLIN: 471 have_read = 1 472 c = self.socket.recv(self.bufsize) 473 Parser_feed(context, c) 474 if Parser_well_formed(context): 475 return default_impl.adoptDocument(Parser_document(context)) 476 477 if have_read: 478 return default_impl.adoptDocument(Parser_document(context)) 479 else: 480 return None 481 482 def write(self, s): 483 484 "Write the plain string 's' to the server." 485 486 self.socket.send(s) 487 488 def send(self, stanza): 489 490 """ 491 Send the 'stanza' to the server. 492 """ 493 494 stanza.toStream(self, encoding=self.encoding) 495 496 def receive(self, timeout=None): 497 498 """ 499 Wait for an incoming stanza, or as long as 'timeout' (in milliseconds), 500 or forever if 'timeout' is omitted or set to None, returning either a 501 stanza document (fragment) or None if nothing was received. 502 """ 503 504 doc = self.read(timeout) 505 if doc is None: 506 return None 507 else: 508 stanza = doc.documentElement 509 510 # Add implied namespace (from the outermost element). 511 # NOTE: This should possibly use the real namespace details from the 512 # NOTE: stream element. 513 514 if stanza.namespaceURI is None: 515 new_stanza = self.createStanza(XMPP_CLIENT_NAMESPACE, stanza.name) 516 new_doc = new_stanza.ownerDocument 517 for attribute in stanza.attributes: 518 new_stanza.attributes.setNamedItemNS(attribute) 519 for child in stanza.childNodes: 520 n = new_doc.importNode(child, 1) 521 new_stanza.appendChild(n) 522 stanza = new_stanza 523 524 return stanza 525 526 # Stanza creation. 527 528 def createAuth(self): 529 return self.createStanza(XMPP_SASL_NAMESPACE, "auth") 530 531 def createIq(self): 532 return self.createStanza(XMPP_CLIENT_NAMESPACE, "iq") 533 534 def createMessage(self): 535 return self.createStanza(XMPP_CLIENT_NAMESPACE, "message") 536 537 def createPresence(self): 538 return self.createStanza(XMPP_CLIENT_NAMESPACE, "presence") 539 540 def createStanza(self, namespaceURI, localName): 541 return createXMPPStanza(namespaceURI, localName) 542 543 # High-level methods. 544 545 def connect(self, host): 546 547 # NOTE: Nasty sending of the raw text because it involves only a start 548 # NOTE: tag. 549 550 self.write(self.connect_str % host) 551 return self.receive() 552 553 def disconnect(self): 554 555 # NOTE: Nasty sending of the raw text because it involves only an end 556 # NOTE: tag. 557 558 self.write(self.disconnect_str) 559 560 # Utility functions. 561 562 createDocument = libxml2dom.createDocument 563 createDocumentType = libxml2dom.createDocumentType 564 565 def createXMPPStanza(namespaceURI, localName): 566 return default_impl.createXMPPStanza(namespaceURI, localName) 567 568 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 569 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 570 571 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 572 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 573 574 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 575 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 576 577 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 578 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 579 580 # Single instance of the implementation. 581 582 default_impl = XMPPImplementation() 583 584 # vim: tabstop=4 expandtab shiftwidth=4