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