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 l = [] 371 fds = self._ready(self.timeout) 372 try: 373 while fds: 374 for fd, status in fds: 375 if fd == self.socket.fileno(): 376 if status & (select.POLLHUP | select.POLLNVAL | select.POLLERR): 377 raise SessionTerminated 378 if status & select.POLLIN: 379 c = self.socket.recv(self.bufsize) 380 if c: 381 l.append(c) 382 else: 383 raise SessionTerminated 384 fds = self.poller.poll(self.timeout) 385 except SessionTerminated: 386 pass 387 return "".join(l) 388 389 def write(self, s): 390 391 "Write the plain string 's' to the server." 392 393 self.socket.send(s) 394 395 def send(self, stanza): 396 397 """ 398 Send the 'stanza' to the server, returning a response stanza if an 399 immediate response was provided, or None otherwise. 400 """ 401 402 stanza.toStream(self, encoding=self.encoding) 403 return self._receive() 404 405 def _receive(self): 406 407 """ 408 Return either a stanza or None, depending on whether anything was read 409 from the server. 410 """ 411 412 s = self.read() 413 if s: 414 return parseString(s).documentElement 415 else: 416 return None 417 418 def receive(self, timeout): 419 420 """ 421 Wait for an incoming stanza, or as long as 'timeout' (in milliseconds), 422 returning either a stanza document (fragment) or None if nothing was 423 received. 424 """ 425 426 if self._ready(timeout): 427 return self._receive() 428 else: 429 return None 430 431 # Stanza creation. 432 433 def createAuth(self): 434 return self.createStanza(XMPP_SASL_NAMESPACE, "auth") 435 436 def createIq(self): 437 return self.createStanza(XMPP_CLIENT_NAMESPACE, "iq") 438 439 def createMessage(self): 440 return self.createStanza(XMPP_CLIENT_NAMESPACE, "message") 441 442 def createStanza(self, namespaceURI, localName): 443 return createXMPPStanza(namespaceURI, localName) 444 445 # High-level methods. 446 447 def connect(self, host): 448 449 # NOTE: Nasty sending of the raw text because it involves only a start 450 # NOTE: tag. 451 452 self.write(self.connect_str % host) 453 return parseString(self.read(), unfinished=1).documentElement 454 455 # Utility functions. 456 457 createDocument = libxml2dom.createDocument 458 createDocumentType = libxml2dom.createDocumentType 459 460 def createXMPPStanza(namespaceURI, localName): 461 return default_impl.createXMPPStanza(namespaceURI, localName) 462 463 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 464 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 465 466 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 467 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 468 469 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 470 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 471 472 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 473 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 474 475 # Single instance of the implementation. 476 477 default_impl = XMPPImplementation() 478 479 # vim: tabstop=4 expandtab shiftwidth=4