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