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