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