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