diff options
author | alkorgun <alkorgun@gmail.com> | 2014-01-20 20:15:56 +0400 |
---|---|---|
committer | alkorgun <alkorgun@gmail.com> | 2014-01-20 20:15:56 +0400 |
commit | 7c407a470a6ab7eb53bdf3727ff591c4e49c7ab4 (patch) | |
tree | fc289aff704c4a364acb7a467360f0b9a5181ed5 | |
parent | 5cf281f5ef5a068de508357ef13d0fcf83ce71a8 (diff) |
some more code cleanup
-rw-r--r-- | xmpp/__init__.py | 84 | ||||
-rw-r--r-- | xmpp/auth.py | 828 | ||||
-rw-r--r-- | xmpp/browser.py | 522 | ||||
-rw-r--r-- | xmpp/client.py | 748 | ||||
-rw-r--r-- | xmpp/commands.py | 896 | ||||
-rw-r--r-- | xmpp/debug.py | 628 | ||||
-rw-r--r-- | xmpp/dispatcher.py | 966 | ||||
-rw-r--r-- | xmpp/features.py | 460 | ||||
-rw-r--r-- | xmpp/filetransfer.py | 452 | ||||
-rw-r--r-- | xmpp/plugin.py | 139 | ||||
-rw-r--r-- | xmpp/protocol.py | 2850 | ||||
-rw-r--r-- | xmpp/roster.py | 560 | ||||
-rw-r--r-- | xmpp/transports.py | 854 |
13 files changed, 4994 insertions, 4993 deletions
diff --git a/xmpp/__init__.py b/xmpp/__init__.py index 831b7c7..e56092b 100644 --- a/xmpp/__init__.py +++ b/xmpp/__init__.py @@ -1,42 +1,42 @@ -# $Id: __init__.py, v1.10 2013/10/21 alkorgun Exp $ - -""" -All features of xmpppy library contained within separate modules. -At present there are modules: -simplexml - XML handling routines -protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines. -debug - Jacob Lundquist's debugging module. Very handy if you like colored debug. -auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport. -transports - low level connection handling. TCP and TLS currently. HTTP support planned. -roster - simple roster for use in clients. -dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas. -features - different stuff that didn't worths separating into modules -browser - DISCO server framework. Allows to build dynamic disco tree. -filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers. - -Most of the classes that is defined in all these modules is an ancestors of -class PlugIn so they share a single set of methods allowing you to compile -a featured XMPP client. For every instance of PlugIn class the 'owner' is the class -in what the plug was plugged. While plugging in such instance usually sets some -methods of owner to it's own ones for easy access. All session specific info stored -either in instance of PlugIn or in owner's instance. This is considered unhandy -and there are plans to port 'Session' class from xmppd.py project for storing all -session-related info. Though if you are not accessing instances variables directly -and use only methods for access all values you should not have any problems. -""" - -import auth -import browser -import commands -import debug -import dispatcher -import features -import filetransfer -import plugin -import protocol -import roster -import simplexml -import transports - -from client import * -from protocol import * +# $Id: __init__.py, v1.10 2013/10/21 alkorgun Exp $
+
+"""
+All features of xmpppy library contained within separate modules.
+At present there are modules:
+simplexml - XML handling routines
+protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines.
+debug - Jacob Lundquist's debugging module. Very handy if you like colored debug.
+auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport.
+transports - low level connection handling. TCP and TLS currently. HTTP support planned.
+roster - simple roster for use in clients.
+dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas.
+features - different stuff that didn't worths separating into modules
+browser - DISCO server framework. Allows to build dynamic disco tree.
+filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers.
+
+Most of the classes that is defined in all these modules is an ancestors of
+class PlugIn so they share a single set of methods allowing you to compile
+a featured XMPP client. For every instance of PlugIn class the 'owner' is the class
+in what the plug was plugged. While plugging in such instance usually sets some
+methods of owner to it's own ones for easy access. All session specific info stored
+either in instance of PlugIn or in owner's instance. This is considered unhandy
+and there are plans to port 'Session' class from xmppd.py project for storing all
+session-related info. Though if you are not accessing instances variables directly
+and use only methods for access all values you should not have any problems.
+"""
+
+from . import auth
+from . import browser
+from . import commands
+from . import debug
+from . import dispatcher
+from . import features
+from . import filetransfer
+from . import plugin
+from . import protocol
+from . import roster
+from . import simplexml
+from . import transports
+
+from .client import *
+from .protocol import *
diff --git a/xmpp/auth.py b/xmpp/auth.py index 7032659..869ead4 100644 --- a/xmpp/auth.py +++ b/xmpp/auth.py @@ -1,414 +1,414 @@ -## auth.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: auth.py, v1.42 2013/10/21 alkorgun Exp $ - -""" -Provides library with all Non-SASL and SASL authentication mechanisms. -Can be used both for client and transport authentication. -""" - -import dispatcher -import hashlib - -from base64 import encodestring, decodestring -from plugin import PlugIn -from protocol import * -from random import random as _random -from re import findall as re_findall - -def HH(some): - return hashlib.md5(some).hexdigest() - -def H(some): - return hashlib.md5(some).digest() - -def C(some): - return ":".join(some) - -class NonSASL(PlugIn): - """ - Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication. - """ - def __init__(self, user, password, resource): - """ - Caches username, password and resource for auth. - """ - PlugIn.__init__(self) - self.DBG_LINE = "gen_auth" - self.user = user - self.password = password - self.resource = resource - - def plugin(self, owner): - """ - Determine the best auth method (digest/0k/plain) and use it for auth. - Returns used method name on success. Used internally. - """ - if not self.resource: - return self.authComponent(owner) - self.DEBUG("Querying server about possible auth methods", "start") - resp = owner.Dispatcher.SendAndWaitForResponse(Iq("get", NS_AUTH, payload=[Node("username", payload=[self.user])])) - if not isResultNode(resp): - self.DEBUG("No result node arrived! Aborting...", "error") - return None - iq = Iq(typ="set", node=resp) - query = iq.getTag("query") - query.setTagData("username", self.user) - query.setTagData("resource", self.resource) - if query.getTag("digest"): - self.DEBUG("Performing digest authentication", "ok") - hash = hashlib.sha1(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest() - query.setTagData("digest", hash) - if query.getTag("password"): - query.delChild("password") - method = "digest" - elif query.getTag("token"): - token = query.getTagData("token") - seq = query.getTagData("sequence") - self.DEBUG("Performing zero-k authentication", "ok") - hash = hashlib.sha1(hashlib.sha1(self.password).hexdigest() + token).hexdigest() - for i in xrange(int(seq)): - hash = hashlib.sha1(hash).hexdigest() - query.setTagData("hash", hash) - method = "0k" - else: - self.DEBUG("Sequre methods unsupported, performing plain text authentication", "warn") - query.setTagData("password", self.password) - method = "plain" - resp = owner.Dispatcher.SendAndWaitForResponse(iq) - if isResultNode(resp): - self.DEBUG("Sucessfully authenticated with remove host.", "ok") - owner.User = self.user - owner.Resource = self.resource - owner._registered_name = owner.User + "@" + owner.Server + "/" + owner.Resource - return method - self.DEBUG("Authentication failed!", "error") - - def authComponent(self, owner): - """ - Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. - """ - self.handshake = 0 - hash = hashlib.sha1(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest() - owner.send(Node(NS_COMPONENT_ACCEPT + " handshake", payload=[hash])) - owner.RegisterHandler("handshake", self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT) - while not self.handshake: - self.DEBUG("waiting on handshake", "notify") - owner.Process(1) - owner._registered_name = self.user - if self.handshake + 1: - return "ok" - - def handshakeHandler(self, disp, stanza): - """ - Handler for registering in dispatcher for accepting transport authentication. - """ - if stanza.getName() == "handshake": - self.handshake = 1 - else: - self.handshake = -1 - -class SASL(PlugIn): - """ - Implements SASL authentication. - """ - def __init__(self, username, password): - PlugIn.__init__(self) - self.username = username - self.password = password - - def plugin(self, owner): - if not self._owner.Dispatcher.Stream._document_attrs.has_key("version"): - self.startsasl = "not-supported" - elif self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self.startsasl = None - - def auth(self): - """ - Start authentication. Result can be obtained via "SASL.startsasl" attribute - and will beeither "success" or "failure". Note that successfull - auth will take at least two Dispatcher.Process() calls. - """ - if self.startsasl: - pass - elif self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - - def plugout(self): - """ - Remove SASL handlers from owner's dispatcher. Used internally. - """ - if self._owner.__dict__.has_key("features"): - self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - if self._owner.__dict__.has_key("challenge"): - self._owner.UnregisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL) - if self._owner.__dict__.has_key("failure"): - self._owner.UnregisterHandler("failure", self.SASLHandler, xmlns=NS_SASL) - if self._owner.__dict__.has_key("success"): - self._owner.UnregisterHandler("success", self.SASLHandler, xmlns=NS_SASL) - - def FeaturesHandler(self, conn, feats): - """ - Used to determine if server supports SASL auth. Used internally. - """ - if not feats.getTag("mechanisms", namespace=NS_SASL): - self.startsasl = "not-supported" - self.DEBUG("SASL not supported by server", "error") - return None - mecs = [] - for mec in feats.getTag("mechanisms", namespace=NS_SASL).getTags("mechanism"): - mecs.append(mec.getData()) - self._owner.RegisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL) - self._owner.RegisterHandler("failure", self.SASLHandler, xmlns=NS_SASL) - self._owner.RegisterHandler("success", self.SASLHandler, xmlns=NS_SASL) - if "ANONYMOUS" in mecs and self.username == None: - node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "ANONYMOUS"}) - elif "DIGEST-MD5" in mecs: - node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "DIGEST-MD5"}) - elif "PLAIN" in mecs: - sasl_data = "%s\x00%s\x00%s" % ("@".join((self.username, self._owner.Server)), self.username, self.password) - node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "PLAIN"}, payload=[encodestring(sasl_data).replace("\r", "").replace("\n", "")]) - else: - self.startsasl = "failure" - self.DEBUG("I can only use DIGEST-MD5 and PLAIN mecanisms.", "error") - return - self.startsasl = "in-process" - self._owner.send(node.__str__()) - raise NodeProcessed() - - def SASLHandler(self, conn, challenge): - """ - Perform next SASL auth step. Used internally. - """ - if challenge.getNamespace() != NS_SASL: - return None - if challenge.getName() == "failure": - self.startsasl = "failure" - try: - reason = challenge.getChildren()[0] - except Exception: - reason = challenge - self.DEBUG("Failed SASL authentification: %s" % reason, "error") - raise NodeProcessed() - elif challenge.getName() == "success": - self.startsasl = "success" - self.DEBUG("Successfully authenticated with remote server.", "ok") - handlers = self._owner.Dispatcher.dumpHandlers() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) - self._owner.Dispatcher.restoreHandlers(handlers) - self._owner.User = self.username - raise NodeProcessed() - incoming_data = challenge.getData() - chal = {} - data = decodestring(incoming_data) - self.DEBUG("Got challenge:" + data, "ok") - for pair in re_findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))', data): - key, value = [x.strip() for x in pair.split("=", 1)] - if value[:1] == '"' and value[-1:] == '"': - value = value[1:-1] - chal[key] = value - if chal.has_key("qop") and "auth" in [x.strip() for x in chal["qop"].split(",")]: - resp = {} - resp["username"] = self.username - resp["realm"] = self._owner.Server - resp["nonce"] = chal["nonce"] - cnonce = "" - for i in xrange(7): - cnonce += hex(int(_random() * 65536 * 4096))[2:] - resp["cnonce"] = cnonce - resp["nc"] = ("00000001") - resp["qop"] = "auth" - resp["digest-uri"] = "xmpp/" + self._owner.Server - A1 = C([H(C([resp["username"], resp["realm"], self.password])), resp["nonce"], resp["cnonce"]]) - A2 = C(["AUTHENTICATE", resp["digest-uri"]]) - response = HH(C([HH(A1), resp["nonce"], resp["nc"], resp["cnonce"], resp["qop"], HH(A2)])) - resp["response"] = response - resp["charset"] = "utf-8" - sasl_data = "" - for key in ("charset", "username", "realm", "nonce", "nc", "cnonce", "digest-uri", "response", "qop"): - if key in ("nc", "qop", "response", "charset"): - sasl_data += "%s=%s," % (key, resp[key]) - else: - sasl_data += "%s=\"%s\"," % (key, resp[key]) - node = Node("response", attrs={"xmlns": NS_SASL}, payload=[encodestring(sasl_data[:-1]).replace("\r", "").replace("\n", "")]) - self._owner.send(node.__str__()) - elif chal.has_key("rspauth"): - self._owner.send(Node("response", attrs={"xmlns": NS_SASL}).__str__()) - else: - self.startsasl = "failure" - self.DEBUG("Failed SASL authentification: unknown challenge", "error") - raise NodeProcessed() - -class Bind(PlugIn): - """ - Bind some JID to the current connection to allow router know of our location. - """ - def __init__(self): - PlugIn.__init__(self) - self.DBG_LINE = "bind" - self.bound = None - - def plugin(self, owner): - """ - Start resource binding, if allowed at this time. Used internally. - """ - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - - def plugout(self): - """ - Remove Bind handler from owner's dispatcher. Used internally. - """ - self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - - def FeaturesHandler(self, conn, feats): - """ - Determine if server supports resource binding and set some internal attributes accordingly. - """ - if not feats.getTag("bind", namespace=NS_BIND): - self.bound = "failure" - self.DEBUG("Server does not requested binding.", "error") - return None - if feats.getTag("session", namespace=NS_SESSION): - self.session = 1 - else: - self.session = -1 - self.bound = [] - - def Bind(self, resource=None): - """ - Perform binding. Use provided resource name or random (if not provided). - """ - while self.bound is None and self._owner.Process(1): - pass - if resource: - resource = [Node("resource", payload=[resource])] - else: - resource = [] - resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("bind", attrs={"xmlns": NS_BIND}, payload=resource)])) - if isResultNode(resp): - self.bound.append(resp.getTag("bind").getTagData("jid")) - self.DEBUG("Successfully bound %s." % self.bound[-1], "ok") - jid = JID(resp.getTag("bind").getTagData("jid")) - self._owner.User = jid.getNode() - self._owner.Resource = jid.getResource() - resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("session", attrs={"xmlns": NS_SESSION})])) - if isResultNode(resp): - self.DEBUG("Successfully opened session.", "ok") - self.session = 1 - return "ok" - else: - self.DEBUG("Session open failed.", "error") - self.session = 0 - elif resp: - self.DEBUG("Binding failed: %s." % resp.getTag("error"), "error") - else: - self.DEBUG("Binding failed: timeout expired.", "error") - return "" - -class ComponentBind(PlugIn): - """ - ComponentBind some JID to the current connection to allow router know of our location. - """ - def __init__(self, sasl): - PlugIn.__init__(self) - self.DBG_LINE = "bind" - self.bound = None - self.needsUnregister = None - self.sasl = sasl - - def plugin(self, owner): - """ - Start resource binding, if allowed at this time. Used internally. - """ - if not self.sasl: - self.bound = [] - return None - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - self.needsUnregister = 1 - - def plugout(self): - """ - Remove ComponentBind handler from owner's dispatcher. Used internally. - """ - if self.needsUnregister: - self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - - def FeaturesHandler(self, conn, feats): - """ - Determine if server supports resource binding and set some internal attributes accordingly. - """ - if not feats.getTag("bind", namespace=NS_BIND): - self.bound = "failure" - self.DEBUG("Server does not requested binding.", "error") - return None - if feats.getTag("session", namespace=NS_SESSION): - self.session = 1 - else: - self.session = -1 - self.bound = [] - - def Bind(self, domain=None): - """ - Perform binding. Use provided domain name (if not provided). - """ - while self.bound is None and self._owner.Process(1): - pass - if self.sasl: - xmlns = NS_COMPONENT_1 - else: - xmlns = None - self.bindresponse = None - ttl = dispatcher.DefaultTimeout - self._owner.RegisterHandler("bind", self.BindHandler, xmlns=xmlns) - self._owner.send(Protocol("bind", attrs={"name": domain}, xmlns=NS_COMPONENT_1)) - while self.bindresponse is None and self._owner.Process(1) and ttl > 0: - ttl -= 1 - self._owner.UnregisterHandler("bind", self.BindHandler, xmlns=xmlns) - resp = self.bindresponse - if resp and resp.getAttr("error"): - self.DEBUG("Binding failed: %s." % resp.getAttr("error"), "error") - elif resp: - self.DEBUG("Successfully bound.", "ok") - return "ok" - else: - self.DEBUG("Binding failed: timeout expired.", "error") - return "" - - def BindHandler(self, conn, bind): - self.bindresponse = bind - pass +## auth.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: auth.py, v1.42 2013/10/21 alkorgun Exp $
+
+"""
+Provides library with all Non-SASL and SASL authentication mechanisms.
+Can be used both for client and transport authentication.
+"""
+
+import hashlib
+from . import dispatcher
+
+from base64 import encodestring, decodestring
+from .plugin import PlugIn
+from .protocol import *
+from random import random as _random
+from re import findall as re_findall
+
+def HH(some):
+ return hashlib.md5(some).hexdigest()
+
+def H(some):
+ return hashlib.md5(some).digest()
+
+def C(some):
+ return ":".join(some)
+
+class NonSASL(PlugIn):
+ """
+ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.
+ """
+ def __init__(self, user, password, resource):
+ """
+ Caches username, password and resource for auth.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "gen_auth"
+ self.user = user
+ self.password = password
+ self.resource = resource
+
+ def plugin(self, owner):
+ """
+ Determine the best auth method (digest/0k/plain) and use it for auth.
+ Returns used method name on success. Used internally.
+ """
+ if not self.resource:
+ return self.authComponent(owner)
+ self.DEBUG("Querying server about possible auth methods", "start")
+ resp = owner.Dispatcher.SendAndWaitForResponse(Iq("get", NS_AUTH, payload=[Node("username", payload=[self.user])]))
+ if not isResultNode(resp):
+ self.DEBUG("No result node arrived! Aborting...", "error")
+ return None
+ iq = Iq(typ="set", node=resp)
+ query = iq.getTag("query")
+ query.setTagData("username", self.user)
+ query.setTagData("resource", self.resource)
+ if query.getTag("digest"):
+ self.DEBUG("Performing digest authentication", "ok")
+ hash = hashlib.sha1(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest()
+ query.setTagData("digest", hash)
+ if query.getTag("password"):
+ query.delChild("password")
+ method = "digest"
+ elif query.getTag("token"):
+ token = query.getTagData("token")
+ seq = query.getTagData("sequence")
+ self.DEBUG("Performing zero-k authentication", "ok")
+ hash = hashlib.sha1(hashlib.sha1(self.password).hexdigest() + token).hexdigest()
+ for i in xrange(int(seq)):
+ hash = hashlib.sha1(hash).hexdigest()
+ query.setTagData("hash", hash)
+ method = "0k"
+ else:
+ self.DEBUG("Sequre methods unsupported, performing plain text authentication", "warn")
+ query.setTagData("password", self.password)
+ method = "plain"
+ resp = owner.Dispatcher.SendAndWaitForResponse(iq)
+ if isResultNode(resp):
+ self.DEBUG("Sucessfully authenticated with remove host.", "ok")
+ owner.User = self.user
+ owner.Resource = self.resource
+ owner._registered_name = owner.User + "@" + owner.Server + "/" + owner.Resource
+ return method
+ self.DEBUG("Authentication failed!", "error")
+
+ def authComponent(self, owner):
+ """
+ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success.
+ """
+ self.handshake = 0
+ hash = hashlib.sha1(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest()
+ owner.send(Node(NS_COMPONENT_ACCEPT + " handshake", payload=[hash]))
+ owner.RegisterHandler("handshake", self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT)
+ while not self.handshake:
+ self.DEBUG("waiting on handshake", "notify")
+ owner.Process(1)
+ owner._registered_name = self.user
+ if self.handshake + 1:
+ return "ok"
+
+ def handshakeHandler(self, disp, stanza):
+ """
+ Handler for registering in dispatcher for accepting transport authentication.
+ """
+ if stanza.getName() == "handshake":
+ self.handshake = 1
+ else:
+ self.handshake = -1
+
+class SASL(PlugIn):
+ """
+ Implements SASL authentication.
+ """
+ def __init__(self, username, password):
+ PlugIn.__init__(self)
+ self.username = username
+ self.password = password
+
+ def plugin(self, owner):
+ if "version" not in self._owner.Dispatcher.Stream._document_attrs:
+ self.startsasl = "not-supported"
+ elif self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self.startsasl = None
+
+ def auth(self):
+ """
+ Start authentication. Result can be obtained via "SASL.startsasl" attribute
+ and will beeither "success" or "failure". Note that successfull
+ auth will take at least two Dispatcher.Process() calls.
+ """
+ if self.startsasl:
+ pass
+ elif self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def plugout(self):
+ """
+ Remove SASL handlers from owner's dispatcher. Used internally.
+ """
+ if hasattr(self._owner, "features"):
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ if hasattr(self._owner, "challenge"):
+ self._owner.UnregisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL)
+ if hasattr(self._owner, "failure"):
+ self._owner.UnregisterHandler("failure", self.SASLHandler, xmlns=NS_SASL)
+ if hasattr(self._owner, "success"):
+ self._owner.UnregisterHandler("success", self.SASLHandler, xmlns=NS_SASL)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Used to determine if server supports SASL auth. Used internally.
+ """
+ if not feats.getTag("mechanisms", namespace=NS_SASL):
+ self.startsasl = "not-supported"
+ self.DEBUG("SASL not supported by server", "error")
+ return None
+ mecs = []
+ for mec in feats.getTag("mechanisms", namespace=NS_SASL).getTags("mechanism"):
+ mecs.append(mec.getData())
+ self._owner.RegisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL)
+ self._owner.RegisterHandler("failure", self.SASLHandler, xmlns=NS_SASL)
+ self._owner.RegisterHandler("success", self.SASLHandler, xmlns=NS_SASL)
+ if "ANONYMOUS" in mecs and self.username == None:
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "ANONYMOUS"})
+ elif "DIGEST-MD5" in mecs:
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "DIGEST-MD5"})
+ elif "PLAIN" in mecs:
+ sasl_data = "%s\x00%s\x00%s" % ("@".join((self.username, self._owner.Server)), self.username, self.password)
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "PLAIN"}, payload=[encodestring(sasl_data).replace("\r", "").replace("\n", "")])
+ else:
+ self.startsasl = "failure"
+ self.DEBUG("I can only use DIGEST-MD5 and PLAIN mecanisms.", "error")
+ return
+ self.startsasl = "in-process"
+ self._owner.send(node.__str__())
+ raise NodeProcessed()
+
+ def SASLHandler(self, conn, challenge):
+ """
+ Perform next SASL auth step. Used internally.
+ """
+ if challenge.getNamespace() != NS_SASL:
+ return None
+ if challenge.getName() == "failure":
+ self.startsasl = "failure"
+ try:
+ reason = challenge.getChildren()[0]
+ except Exception:
+ reason = challenge
+ self.DEBUG("Failed SASL authentification: %s" % reason, "error")
+ raise NodeProcessed()
+ elif challenge.getName() == "success":
+ self.startsasl = "success"
+ self.DEBUG("Successfully authenticated with remote server.", "ok")
+ handlers = self._owner.Dispatcher.dumpHandlers()
+ self._owner.Dispatcher.PlugOut()
+ dispatcher.Dispatcher().PlugIn(self._owner)
+ self._owner.Dispatcher.restoreHandlers(handlers)
+ self._owner.User = self.username
+ raise NodeProcessed()
+ incoming_data = challenge.getData()
+ chal = {}
+ data = decodestring(incoming_data)
+ self.DEBUG("Got challenge:" + data, "ok")
+ for pair in re_findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))', data):
+ key, value = [x.strip() for x in pair.split("=", 1)]
+ if value[:1] == '"' and value[-1:] == '"':
+ value = value[1:-1]
+ chal[key] = value
+ if "qop" in chal and "auth" in [x.strip() for x in chal["qop"].split(",")]:
+ resp = {}
+ resp["username"] = self.username
+ resp["realm"] = self._owner.Server
+ resp["nonce"] = chal["nonce"]
+ cnonce = ""
+ for i in xrange(7):
+ cnonce += hex(int(_random() * 65536 * 4096))[2:]
+ resp["cnonce"] = cnonce
+ resp["nc"] = ("00000001")
+ resp["qop"] = "auth"
+ resp["digest-uri"] = "xmpp/" + self._owner.Server
+ A1 = C([H(C([resp["username"], resp["realm"], self.password])), resp["nonce"], resp["cnonce"]])
+ A2 = C(["AUTHENTICATE", resp["digest-uri"]])
+ response = HH(C([HH(A1), resp["nonce"], resp["nc"], resp["cnonce"], resp["qop"], HH(A2)]))
+ resp["response"] = response
+ resp["charset"] = "utf-8"
+ sasl_data = ""
+ for key in ("charset", "username", "realm", "nonce", "nc", "cnonce", "digest-uri", "response", "qop"):
+ if key in ("nc", "qop", "response", "charset"):
+ sasl_data += "%s=%s," % (key, resp[key])
+ else:
+ sasl_data += "%s=\"%s\"," % (key, resp[key])
+ node = Node("response", attrs={"xmlns": NS_SASL}, payload=[encodestring(sasl_data[:-1]).replace("\r", "").replace("\n", "")])
+ self._owner.send(node.__str__())
+ elif "rspauth" in chal:
+ self._owner.send(Node("response", attrs={"xmlns": NS_SASL}).__str__())
+ else:
+ self.startsasl = "failure"
+ self.DEBUG("Failed SASL authentification: unknown challenge", "error")
+ raise NodeProcessed()
+
+class Bind(PlugIn):
+ """
+ Bind some JID to the current connection to allow router know of our location.
+ """
+ def __init__(self):
+ PlugIn.__init__(self)
+ self.DBG_LINE = "bind"
+ self.bound = None
+
+ def plugin(self, owner):
+ """
+ Start resource binding, if allowed at this time. Used internally.
+ """
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def plugout(self):
+ """
+ Remove Bind handler from owner's dispatcher. Used internally.
+ """
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Determine if server supports resource binding and set some internal attributes accordingly.
+ """
+ if not feats.getTag("bind", namespace=NS_BIND):
+ self.bound = "failure"
+ self.DEBUG("Server does not requested binding.", "error")
+ return None
+ if feats.getTag("session", namespace=NS_SESSION):
+ self.session = 1
+ else:
+ self.session = -1
+ self.bound = []
+
+ def Bind(self, resource=None):
+ """
+ Perform binding. Use provided resource name or random (if not provided).
+ """
+ while self.bound is None and self._owner.Process(1):
+ pass
+ if resource:
+ resource = [Node("resource", payload=[resource])]
+ else:
+ resource = []
+ resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("bind", attrs={"xmlns": NS_BIND}, payload=resource)]))
+ if isResultNode(resp):
+ self.bound.append(resp.getTag("bind").getTagData("jid"))
+ self.DEBUG("Successfully bound %s." % self.bound[-1], "ok")
+ jid = JID(resp.getTag("bind").getTagData("jid"))
+ self._owner.User = jid.getNode()
+ self._owner.Resource = jid.getResource()
+ resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("session", attrs={"xmlns": NS_SESSION})]))
+ if isResultNode(resp):
+ self.DEBUG("Successfully opened session.", "ok")
+ self.session = 1
+ return "ok"
+ else:
+ self.DEBUG("Session open failed.", "error")
+ self.session = 0
+ elif resp:
+ self.DEBUG("Binding failed: %s." % resp.getTag("error"), "error")
+ else:
+ self.DEBUG("Binding failed: timeout expired.", "error")
+ return ""
+
+class ComponentBind(PlugIn):
+ """
+ ComponentBind some JID to the current connection to allow router know of our location.
+ """
+ def __init__(self, sasl):
+ PlugIn.__init__(self)
+ self.DBG_LINE = "bind"
+ self.bound = None
+ self.needsUnregister = None
+ self.sasl = sasl
+
+ def plugin(self, owner):
+ """
+ Start resource binding, if allowed at this time. Used internally.
+ """
+ if not self.sasl:
+ self.bound = []
+ return None
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self.needsUnregister = 1
+
+ def plugout(self):
+ """
+ Remove ComponentBind handler from owner's dispatcher. Used internally.
+ """
+ if self.needsUnregister:
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Determine if server supports resource binding and set some internal attributes accordingly.
+ """
+ if not feats.getTag("bind", namespace=NS_BIND):
+ self.bound = "failure"
+ self.DEBUG("Server does not requested binding.", "error")
+ return None
+ if feats.getTag("session", namespace=NS_SESSION):
+ self.session = 1
+ else:
+ self.session = -1
+ self.bound = []
+
+ def Bind(self, domain=None):
+ """
+ Perform binding. Use provided domain name (if not provided).
+ """
+ while self.bound is None and self._owner.Process(1):
+ pass
+ if self.sasl:
+ xmlns = NS_COMPONENT_1
+ else:
+ xmlns = None
+ self.bindresponse = None
+ ttl = dispatcher.DefaultTimeout
+ self._owner.RegisterHandler("bind", self.BindHandler, xmlns=xmlns)
+ self._owner.send(Protocol("bind", attrs={"name": domain}, xmlns=NS_COMPONENT_1))
+ while self.bindresponse is None and self._owner.Process(1) and ttl > 0:
+ ttl -= 1
+ self._owner.UnregisterHandler("bind", self.BindHandler, xmlns=xmlns)
+ resp = self.bindresponse
+ if resp and resp.getAttr("error"):
+ self.DEBUG("Binding failed: %s." % resp.getAttr("error"), "error")
+ elif resp:
+ self.DEBUG("Successfully bound.", "ok")
+ return "ok"
+ else:
+ self.DEBUG("Binding failed: timeout expired.", "error")
+ return ""
+
+ def BindHandler(self, conn, bind):
+ self.bindresponse = bind
+ pass
diff --git a/xmpp/browser.py b/xmpp/browser.py index db74131..5ecae1e 100644 --- a/xmpp/browser.py +++ b/xmpp/browser.py @@ -1,261 +1,261 @@ -## browser.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: browser.py, v1.13 2013/11/03 alkorgun Exp $ - -""" -Browser module provides DISCO server framework for your application. -This functionality can be used for very different purposes - from publishing -software version and supported features to building of "jabber site" that users -can navigate with their disco browsers and interact with active content. - -Such functionality is achieved via registering "DISCO handlers" that are -automatically called when user requests some node of your disco tree. -""" - -from dispatcher import * -from plugin import PlugIn - -class Browser(PlugIn): - """ - WARNING! This class is for components only. It will not work in client mode! - - Standart xmpppy class that is ancestor of PlugIn and can be attached - to your application. - All processing will be performed in the handlers registered in the browser - instance. You can register any number of handlers ensuring that for each - node/jid combination only one (or none) handler registered. - You can register static information or the fully-blown function that will - calculate the answer dynamically. - Example of static info (see JEP-0030, examples 13-14): - # cl - your xmpppy connection instance. - b = xmpp.browser.Browser() - b.PlugIn(cl) - items = [] - item = {} - item["jid"] = "catalog.shakespeare.lit" - item["node"] = "books" - item["name"] = "Books by and about Shakespeare" - items.append(item) - item = {} - item["jid"] = "catalog.shakespeare.lit" - item["node"] = "clothing" - item["name"] = "Wear your literary taste with pride" - items.append(item) - item = {} - item["jid"] = "catalog.shakespeare.lit" - item["node"] = "music" - item["name"] = "Music from the time of Shakespeare" - items.append(item) - info = {"ids": [], "features": []} - b.setDiscoHandler({"items": items, "info": info}) - - items should be a list of item elements. - every item element can have any of these four keys: "jid", "node", "name", "action" - info should be a dicionary and must have keys "ids" and "features". - Both of them should be lists: - ids is a list of dictionaries and features is a list of text strings. - Example (see JEP-0030, examples 1-2) - # cl - your xmpppy connection instance. - b = xmpp.browser.Browser() - b.PlugIn(cl) - items = [] - ids = [] - ids.append({"category": "conference", "type": "text", "name": "Play-Specific Chatrooms"}) - ids.append({"category": "directory", "type": "chatroom", "name": "Play-Specific Chatrooms"}) - features = [ - NS_DISCO_INFO, - NS_DISCO_ITEMS, - NS_MUC, - NS_REGISTER, - NS_SEARCH, - NS_TIME, - NS_VERSION - ] - info = {"ids": ids, "features": features} - # info["xdata"] = xmpp.protocol.DataForm() # JEP-0128 - b.setDiscoHandler({"items": [], "info": info}) - """ - def __init__(self): - """ - Initialises internal variables. Used internally. - """ - PlugIn.__init__(self) - DBG_LINE = "browser" - self._exported_methods = [] - self._handlers = {"": {}} - - def plugin(self, owner): - """ - Registers it's own iq handlers in your application dispatcher instance. - Used internally. - """ - owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO) - owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS) - - def plugout(self): - """ - Unregisters browser's iq handlers from your application dispatcher instance. - Used internally. - """ - self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO) - self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS) - - def _traversePath(self, node, jid, set=0): - """ - Returns dictionary and key or None,None - None - root node (w/o "node" attribute) - /a/b/c - node - /a/b/ - branch - Set returns "" or None as the key - get returns "" or None as the key or None as the dict. - Used internally. - """ - if jid in self._handlers: - cur = self._handlers[jid] - elif set: - self._handlers[jid] = {} - cur = self._handlers[jid] - else: - cur = self._handlers[""] - if node is None: - node = [None] - else: - node = node.replace("/", " /").split("/") - for i in node: - if i != "" and cur.has_key(i): - cur = cur[i] - elif set and i != "": - cur[i] = {dict: cur, str: i} - cur = cur[i] - elif set or cur.has_key(""): - return cur, "" - else: - return None, None - if cur.has_key(1) or set: - return cur, 1 - raise Exception("Corrupted data") - - def setDiscoHandler(self, handler, node="", jid=""): - """ - This is the main method that you will use in this class. - It is used to register supplied DISCO handler (or dictionary with static info) - as handler of some disco tree branch. - If you do not specify the node this handler will be used for all queried nodes. - If you do not specify the jid this handler will be used for all queried JIDs. - - Usage: - cl.Browser.setDiscoHandler(someDict, node, jid) - or - cl.Browser.setDiscoHandler(someDISCOHandler, node, jid) - where - - someDict = { - "items":[ - {"jid": "jid2", "action": "action2", "node":"node2", "name": "name2"}, - {"jid": "jid4", "node": "node4"} - ], - "info" :{ - "ids":[ - {"category":" category1", "type": "type1", "name": "name1"}, - {"category":" category3", "type": "type3", "name": "name3"}, - ], - "features": ["feature1", "feature2", "feature3", "feature4"], - "xdata": DataForm - } - } - - and/or - - def someDISCOHandler(session,request,TYR): - # if TYR == "items": # returns items list of the same format as shown above - # elif TYR == "info": # returns info dictionary of the same format as shown above - # else: # this case is impossible for now. - """ - self.DEBUG("Registering handler %s for \"%s\" node->%s" % (handler, jid, node), "info") - node, key = self._traversePath(node, jid, 1) - node[key] = handler - - def getDiscoHandler(self, node="", jid=""): - """ - Returns the previously registered DISCO handler - that is resonsible for this node/jid combination. - Used internally. - """ - node, key = self._traversePath(node, jid) - if node: - return node[key] - - def delDiscoHandler(self, node="", jid=""): - """ - Unregisters DISCO handler that is resonsible for this - node/jid combination. When handler is unregistered the branch - is handled in the same way that it's parent branch from this moment. - """ - node, key = self._traversePath(node, jid) - if node: - handler = node[key] - del node[dict][node[str]] - return handler - - def _DiscoveryHandler(self, conn, request): - """ - Servers DISCO iq request from the remote client. - Automatically determines the best handler to use and calls it - (to handle the request. Used internally. - """ - node = request.getQuerynode() - if node: - nodestr = node - else: - nodestr = "None" - handler = self.getDiscoHandler(node, request.getTo()) - if not handler: - self.DEBUG("No Handler for request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "error") - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - self.DEBUG("Handling request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "ok") - rep = request.buildReply("result") - if node: - rep.setQuerynode(node) - q = rep.getTag("query") - if request.getQueryNS() == NS_DISCO_ITEMS: - # handler must return list: [{jid, action, node, name}] - if isinstance(handler, dict): - lst = handler["items"] - else: - lst = handler(conn, request, "items") - if lst == None: - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - for item in lst: - q.addChild("item", item) - elif request.getQueryNS() == NS_DISCO_INFO: - if isinstance(handler, dict): - dt = handler["info"] - else: - dt = handler(conn, request, "info") - if dt == None: - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - # handler must return dictionary: - # {"ids": [{}, {}, {}, {}], "features": [fe, at, ur, es], "xdata": DataForm} - for id in dt["ids"]: - q.addChild("identity", id) - for feature in dt["features"]: - q.addChild("feature", {"var": feature}) - if dt.has_key("xdata"): - q.addChild(node=dt["xdata"]) - conn.send(rep) - raise NodeProcessed() +## browser.py
+##
+## Copyright (C) 2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: browser.py, v1.13 2013/11/03 alkorgun Exp $
+
+"""
+Browser module provides DISCO server framework for your application.
+This functionality can be used for very different purposes - from publishing
+software version and supported features to building of "jabber site" that users
+can navigate with their disco browsers and interact with active content.
+
+Such functionality is achieved via registering "DISCO handlers" that are
+automatically called when user requests some node of your disco tree.
+"""
+
+from .dispatcher import *
+from .plugin import PlugIn
+
+class Browser(PlugIn):
+ """
+ WARNING! This class is for components only. It will not work in client mode!
+
+ Standart xmpppy class that is ancestor of PlugIn and can be attached
+ to your application.
+ All processing will be performed in the handlers registered in the browser
+ instance. You can register any number of handlers ensuring that for each
+ node/jid combination only one (or none) handler registered.
+ You can register static information or the fully-blown function that will
+ calculate the answer dynamically.
+ Example of static info (see JEP-0030, examples 13-14):
+ # cl - your xmpppy connection instance.
+ b = xmpp.browser.Browser()
+ b.PlugIn(cl)
+ items = []
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "books"
+ item["name"] = "Books by and about Shakespeare"
+ items.append(item)
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "clothing"
+ item["name"] = "Wear your literary taste with pride"
+ items.append(item)
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "music"
+ item["name"] = "Music from the time of Shakespeare"
+ items.append(item)
+ info = {"ids": [], "features": []}
+ b.setDiscoHandler({"items": items, "info": info})
+
+ items should be a list of item elements.
+ every item element can have any of these four keys: "jid", "node", "name", "action"
+ info should be a dicionary and must have keys "ids" and "features".
+ Both of them should be lists:
+ ids is a list of dictionaries and features is a list of text strings.
+ Example (see JEP-0030, examples 1-2)
+ # cl - your xmpppy connection instance.
+ b = xmpp.browser.Browser()
+ b.PlugIn(cl)
+ items = []
+ ids = []
+ ids.append({"category": "conference", "type": "text", "name": "Play-Specific Chatrooms"})
+ ids.append({"category": "directory", "type": "chatroom", "name": "Play-Specific Chatrooms"})
+ features = [
+ NS_DISCO_INFO,
+ NS_DISCO_ITEMS,
+ NS_MUC,
+ NS_REGISTER,
+ NS_SEARCH,
+ NS_TIME,
+ NS_VERSION
+ ]
+ info = {"ids": ids, "features": features}
+ # info["xdata"] = xmpp.protocol.DataForm() # JEP-0128
+ b.setDiscoHandler({"items": [], "info": info})
+ """
+ def __init__(self):
+ """
+ Initialises internal variables. Used internally.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "browser"
+ self._exported_methods = []
+ self._handlers = {"": {}}
+
+ def plugin(self, owner):
+ """
+ Registers it's own iq handlers in your application dispatcher instance.
+ Used internally.
+ """
+ owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO)
+ owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS)
+
+ def plugout(self):
+ """
+ Unregisters browser's iq handlers from your application dispatcher instance.
+ Used internally.
+ """
+ self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO)
+ self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS)
+
+ def _traversePath(self, node, jid, set=0):
+ """
+ Returns dictionary and key or None,None
+ None - root node (w/o "node" attribute)
+ /a/b/c - node
+ /a/b/ - branch
+ Set returns "" or None as the key
+ get returns "" or None as the key or None as the dict.
+ Used internally.
+ """
+ if jid in self._handlers:
+ cur = self._handlers[jid]
+ elif set:
+ self._handlers[jid] = {}
+ cur = self._handlers[jid]
+ else:
+ cur = self._handlers[""]
+ if node is None:
+ node = [None]
+ else:
+ node = node.replace("/", " /").split("/")
+ for i in node:
+ if i != "" and i in cur:
+ cur = cur[i]
+ elif set and i != "":
+ cur[i] = {dict: cur, str: i}
+ cur = cur[i]
+ elif set or "" in cur:
+ return cur, ""
+ else:
+ return None, None
+ if 1 in cur or set:
+ return cur, 1
+ raise Exception("Corrupted data")
+
+ def setDiscoHandler(self, handler, node="", jid=""):
+ """
+ This is the main method that you will use in this class.
+ It is used to register supplied DISCO handler (or dictionary with static info)
+ as handler of some disco tree branch.
+ If you do not specify the node this handler will be used for all queried nodes.
+ If you do not specify the jid this handler will be used for all queried JIDs.
+
+ Usage:
+ cl.Browser.setDiscoHandler(someDict, node, jid)
+ or
+ cl.Browser.setDiscoHandler(someDISCOHandler, node, jid)
+ where
+
+ someDict = {
+ "items":[
+ {"jid": "jid2", "action": "action2", "node":"node2", "name": "name2"},
+ {"jid": "jid4", "node": "node4"}
+ ],
+ "info" :{
+ "ids":[
+ {"category":" category1", "type": "type1", "name": "name1"},
+ {"category":" category3", "type": "type3", "name": "name3"},
+ ],
+ "features": ["feature1", "feature2", "feature3", "feature4"],
+ "xdata": DataForm
+ }
+ }
+
+ and/or
+
+ def someDISCOHandler(session,request,TYR):
+ # if TYR == "items": # returns items list of the same format as shown above
+ # elif TYR == "info": # returns info dictionary of the same format as shown above
+ # else: # this case is impossible for now.
+ """
+ self.DEBUG("Registering handler %s for \"%s\" node->%s" % (handler, jid, node), "info")
+ node, key = self._traversePath(node, jid, 1)
+ node[key] = handler
+
+ def getDiscoHandler(self, node="", jid=""):
+ """
+ Returns the previously registered DISCO handler
+ that is resonsible for this node/jid combination.
+ Used internally.
+ """
+ node, key = self._traversePath(node, jid)
+ if node:
+ return node[key]
+
+ def delDiscoHandler(self, node="", jid=""):
+ """
+ Unregisters DISCO handler that is resonsible for this
+ node/jid combination. When handler is unregistered the branch
+ is handled in the same way that it's parent branch from this moment.
+ """
+ node, key = self._traversePath(node, jid)
+ if node:
+ handler = node[key]
+ del node[dict][node[str]]
+ return handler
+
+ def _DiscoveryHandler(self, conn, request):
+ """
+ Servers DISCO iq request from the remote client.
+ Automatically determines the best handler to use and calls it
+ (to handle the request. Used internally.
+ """
+ node = request.getQuerynode()
+ if node:
+ nodestr = node
+ else:
+ nodestr = "None"
+ handler = self.getDiscoHandler(node, request.getTo())
+ if not handler:
+ self.DEBUG("No Handler for request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "error")
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ self.DEBUG("Handling request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "ok")
+ rep = request.buildReply("result")
+ if node:
+ rep.setQuerynode(node)
+ q = rep.getTag("query")
+ if request.getQueryNS() == NS_DISCO_ITEMS:
+ # handler must return list: [{jid, action, node, name}]
+ if isinstance(handler, dict):
+ lst = handler["items"]
+ else:
+ lst = handler(conn, request, "items")
+ if lst == None:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ for item in lst:
+ q.addChild("item", item)
+ elif request.getQueryNS() == NS_DISCO_INFO:
+ if isinstance(handler, dict):
+ dt = handler["info"]
+ else:
+ dt = handler(conn, request, "info")
+ if dt == None:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ # handler must return dictionary:
+ # {"ids": [{}, {}, {}, {}], "features": [fe, at, ur, es], "xdata": DataForm}
+ for id in dt["ids"]:
+ q.addChild("identity", id)
+ for feature in dt["features"]:
+ q.addChild("feature", {"var": feature})
+ if "xdata" in dt:
+ q.addChild(node=dt["xdata"])
+ conn.send(rep)
+ raise NodeProcessed()
diff --git a/xmpp/client.py b/xmpp/client.py index 66806ea..cf88332 100644 --- a/xmpp/client.py +++ b/xmpp/client.py @@ -1,374 +1,374 @@ -## client.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: client.py, v1.62 2013/10/21 alkorgun Exp $ - -""" -Provides PlugIn class functionality to develop extentions for xmpppy. -Also provides Client and Component classes implementations as the -examples of xmpppy structures usage. -These classes can be used for simple applications "AS IS" though. -""" - -import debug -import transports -import dispatcher -import auth -import roster - -from plugin import PlugIn - -Debug = debug -Debug.DEBUGGING_IS_ON = 1 - -Debug.Debug.colors["socket"] = debug.color_dark_gray -Debug.Debug.colors["CONNECTproxy"] = debug.color_dark_gray -Debug.Debug.colors["nodebuilder"] = debug.color_brown -Debug.Debug.colors["client"] = debug.color_cyan -Debug.Debug.colors["component"] = debug.color_cyan -Debug.Debug.colors["dispatcher"] = debug.color_green -Debug.Debug.colors["browser"] = debug.color_blue -Debug.Debug.colors["auth"] = debug.color_yellow -Debug.Debug.colors["roster"] = debug.color_magenta -Debug.Debug.colors["ibb"] = debug.color_yellow -Debug.Debug.colors["down"] = debug.color_brown -Debug.Debug.colors["up"] = debug.color_brown -Debug.Debug.colors["data"] = debug.color_brown -Debug.Debug.colors["ok"] = debug.color_green -Debug.Debug.colors["warn"] = debug.color_yellow -Debug.Debug.colors["error"] = debug.color_red -Debug.Debug.colors["start"] = debug.color_dark_gray -Debug.Debug.colors["stop"] = debug.color_dark_gray -Debug.Debug.colors["sent"] = debug.color_yellow -Debug.Debug.colors["got"] = debug.color_bright_cyan - -DBG_CLIENT = "client" -DBG_COMPONENT = "component" - - -class CommonClient: - """ - Base for Client and Component classes. - """ - def __init__(self, server, port=5222, debug=["always", "nodebuilder"]): - """ - Caches server name and (optionally) port to connect to. "debug" parameter specifies - the debug IDs that will go into debug output. You can either specifiy an "include" - or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. - Full list: ["nodebuilder", "dispatcher", "gen_auth", "SASL_auth", "bind", "socket", - "CONNECTproxy", "TLS", "roster", "browser", "ibb"]. - """ - if isinstance(self, Client): - self.Namespace, self.DBG = "jabber:client", DBG_CLIENT - elif isinstance(self, Component): - self.Namespace, self.DBG = dispatcher.NS_COMPONENT_ACCEPT, DBG_COMPONENT - self.defaultNamespace = self.Namespace - self.disconnect_handlers = [] - self.Server = server - self.Port = port - if debug and not isinstance(debug, list): - debug = ["always", "nodebuilder"] - self._DEBUG = Debug.Debug(debug) - self.DEBUG = self._DEBUG.Show - self.debug_flags = self._DEBUG.debug_flags - self.debug_flags.append(self.DBG) - self._owner = self - self._registered_name = None - self.RegisterDisconnectHandler(self.DisconnectHandler) - self.connected = "" - self._route = 0 - - def RegisterDisconnectHandler(self, handler): - """ - Register handler that will be called on disconnect. - """ - self.disconnect_handlers.append(handler) - - def UnregisterDisconnectHandler(self, handler): - """ - Unregister handler that is called on disconnect. - """ - self.disconnect_handlers.remove(handler) - - def disconnected(self): - """ - Called on disconnection. Calls disconnect handlers and cleans things up. - """ - self.connected = "" - self.DEBUG(self.DBG, "Disconnect detected", "stop") - self.disconnect_handlers.reverse() - for dhnd in self.disconnect_handlers: - dhnd() - self.disconnect_handlers.reverse() - if self.__dict__.has_key("TLS"): - self.TLS.PlugOut() - - def DisconnectHandler(self): - """ - Default disconnect handler. Just raises an IOError. - If you choosed to use this class in your production client, - override this method or at least unregister it. - """ - raise IOError("Disconnected!") - - def event(self, eventName, args={}): - """ - Default event handler. To be overriden. - """ - print("Event: %s-%s" % (eventName, args)) - - def isConnected(self): - """ - Returns connection state. F.e.: None / "tls" / "tcp+non_sasl" . - """ - return self.connected - - def reconnectAndReauth(self, handlerssave=None): - """ - Example of reconnection method. In fact, it can be used to batch connection and auth as well. - """ - Dispatcher_ = False - if not handlerssave: - Dispatcher_, handlerssave = True, self.Dispatcher.dumpHandlers() - if self.__dict__.has_key("ComponentBind"): - self.ComponentBind.PlugOut() - if self.__dict__.has_key("Bind"): - self.Bind.PlugOut() - self._route = 0 - if self.__dict__.has_key("NonSASL"): - self.NonSASL.PlugOut() - if self.__dict__.has_key("SASL"): - self.SASL.PlugOut() - if self.__dict__.has_key("TLS"): - self.TLS.PlugOut() - if Dispatcher_: - self.Dispatcher.PlugOut() - if self.__dict__.has_key("HTTPPROXYsocket"): - self.HTTPPROXYsocket.PlugOut() - if self.__dict__.has_key("TCPsocket"): - self.TCPsocket.PlugOut() - if not self.connect(server=self._Server, proxy=self._Proxy): - return None - if not self.auth(self._User, self._Password, self._Resource): - return None - self.Dispatcher.restoreHandlers(handlerssave) - return self.connected - - def connect(self, server=None, proxy=None, ssl=None, use_srv=False): - """ - Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. - Returns None or "tcp" or "tls", depending on the result. - """ - if not server: - server = (self.Server, self.Port) - if proxy: - sock = transports.HTTPPROXYsocket(proxy, server, use_srv) - else: - sock = transports.TCPsocket(server, use_srv) - connected = sock.PlugIn(self) - if not connected: - sock.PlugOut() - return None - self._Server, self._Proxy = server, proxy - self.connected = "tcp" - if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl: - try: # FIXME. This should be done in transports.py - transports.TLS().PlugIn(self, now=1) - self.connected = "ssl" - except transports.socket.sslerror: - return None - dispatcher.Dispatcher().PlugIn(self) - while self.Dispatcher.Stream._document_attrs is None: - if not self.Process(1): - return None - if self.Dispatcher.Stream._document_attrs.has_key("version") and self.Dispatcher.Stream._document_attrs["version"] == "1.0": - while not self.Dispatcher.Stream.features and self.Process(1): - pass # If we get version 1.0 stream the features tag MUST BE presented - return self.connected - -class Client(CommonClient): - """ - Example client class, based on CommonClient. - """ - def connect(self, server=None, proxy=None, secure=None, use_srv=True): - """ - Connect to jabber server. If you want to specify different ip/port to connect to you can - pass it as tuple as first parameter. If there is HTTP proxy between you and server - specify it's address and credentials (if needed) in the second argument. - If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) - If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. - If you want to disable tls/ssl support completely, set it to 0. - Example: connect(("192.168.5.5", 5222), {"host": "proxy.my.net", "port": 8080, "user": "me", "password": "secret"}) - Returns "" or "tcp" or "tls", depending on the result. - """ - if not CommonClient.connect(self, server, proxy, secure, use_srv) or secure != None and not secure: - return self.connected - transports.TLS().PlugIn(self) - if not hasattr(self, "Dispatcher"): - return None - if not self.Dispatcher.Stream._document_attrs.has_key("version") or not self.Dispatcher.Stream._document_attrs["version"] == "1.0": - return self.connected - while not self.Dispatcher.Stream.features and self.Process(1): - pass # If we get version 1.0 stream the features tag MUST BE presented - if not self.Dispatcher.Stream.features.getTag("starttls"): - return self.connected # TLS not supported by server - while not self.TLS.starttls and self.Process(1): - pass - if not hasattr(self, "TLS") or self.TLS.starttls != "success": - self.event("tls_failed"); return self.connected - self.connected = "tls" - return self.connected - - def auth(self, user, password, resource="", sasl=1): - """ - Authenticate connnection and bind resource. If resource is not provided - random one or library name used. - """ - self._User, self._Password, self._Resource = user, password, resource - while not self.Dispatcher.Stream._document_attrs and self.Process(1): - pass - if self.Dispatcher.Stream._document_attrs.has_key("version") and self.Dispatcher.Stream._document_attrs["version"] == "1.0": - while not self.Dispatcher.Stream.features and self.Process(1): - pass # If we get version 1.0 stream the features tag MUST BE presented - if sasl: - auth.SASL(user, password).PlugIn(self) - if not sasl or self.SASL.startsasl == "not-supported": - if not resource: - resource = "xmpppy" - if auth.NonSASL(user, password, resource).PlugIn(self): - self.connected += "+old_auth" - return "old_auth" - return None - self.SASL.auth() - while self.SASL.startsasl == "in-process" and self.Process(1): - pass - if self.SASL.startsasl == "success": - auth.Bind().PlugIn(self) - while self.Bind.bound is None and self.Process(1): - pass - if self.Bind.Bind(resource): - self.connected += "+sasl" - return "sasl" - elif self.__dict__.has_key("SASL"): - self.SASL.PlugOut() - - def getRoster(self): - """ - Return the Roster instance, previously plugging it in and - requesting roster from server if needed. - """ - if not self.__dict__.has_key("Roster"): - roster.Roster().PlugIn(self) - return self.Roster.getRoster() - - def sendInitPresence(self, requestRoster=1): - """ - Send roster request and initial <presence/>. - You can disable the first by setting requestRoster argument to 0. - """ - self.sendPresence(requestRoster=requestRoster) - - def sendPresence(self, jid=None, typ=None, requestRoster=0): - """ - Send some specific presence state. - Can also request roster from server if according agrument is set. - """ - if requestRoster: - roster.Roster().PlugIn(self) - self.send(dispatcher.Presence(to=jid, typ=typ)) - -class Component(CommonClient): - """ - Component class. The only difference from CommonClient is ability to perform component authentication. - """ - def __init__(self, transport, port=5347, typ=None, debug=["always", "nodebuilder"], domains=None, sasl=0, bind=0, route=0, xcp=0): - """ - Init function for Components. - As components use a different auth mechanism which includes the namespace of the component. - Jabberd1.4 and Ejabberd use the default namespace then for all client messages. - Jabberd2 uses jabber:client. - "transport" argument is a transport name that you are going to serve (f.e. "irc.localhost"). - "port" can be specified if "transport" resolves to correct IP. If it is not then you'll have to specify IP - and port while calling "connect()". - If you are going to serve several different domains with single Component instance - you must list them ALL - in the "domains" argument. - For jabberd2 servers you should set typ="jabberd2" argument. - """ - CommonClient.__init__(self, transport, port=port, debug=debug) - self.typ = typ - self.sasl = sasl - self.bind = bind - self.route = route - self.xcp = xcp - if domains: - self.domains = domains - else: - self.domains = [transport] - - def connect(self, server=None, proxy=None): - """ - This will connect to the server, and if the features tag is found then set - the namespace to be jabber:client as that is required for jabberd2. - "server" and "proxy" arguments have the same meaning as in xmpp.Client.connect(). - """ - if self.sasl: - self.Namespace = auth.NS_COMPONENT_1 - self.Server = server[0] - CommonClient.connect(self, server=server, proxy=proxy) - if self.connected and (self.typ == "jabberd2" or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp): - self.defaultNamespace = auth.NS_CLIENT - self.Dispatcher.RegisterNamespace(self.defaultNamespace) - self.Dispatcher.RegisterProtocol("iq", dispatcher.Iq) - self.Dispatcher.RegisterProtocol("message", dispatcher.Message) - self.Dispatcher.RegisterProtocol("presence", dispatcher.Presence) - return self.connected - - def dobind(self, sasl): - # This has to be done before binding, because we can receive a route stanza before binding finishes - self._route = self.route - if self.bind: - for domain in self.domains: - auth.ComponentBind(sasl).PlugIn(self) - while self.ComponentBind.bound is None: - self.Process(1) - if (not self.ComponentBind.Bind(domain)): - self.ComponentBind.PlugOut() - return None - self.ComponentBind.PlugOut() - - def auth(self, name, password, dup=None): - """ - Authenticate component "name" with password "password". - """ - self._User, self._Password, self._Resource = name, password, "" - try: - if self.sasl: - auth.SASL(name, password).PlugIn(self) - if not self.sasl or self.SASL.startsasl == "not-supported": - if auth.NonSASL(name, password, "").PlugIn(self): - self.dobind(sasl=False) - self.connected += "+old_auth" - return "old_auth" - return None - self.SASL.auth() - while self.SASL.startsasl == "in-process" and self.Process(1): - pass - if self.SASL.startsasl == "success": - self.dobind(sasl=True) - self.connected += "+sasl" - return "sasl" - else: - raise auth.NotAuthorized(self.SASL.startsasl) - except Exception: - self.DEBUG(self.DBG, "Failed to authenticate %s" % name, "error") +## client.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: client.py, v1.62 2013/10/21 alkorgun Exp $
+
+"""
+Provides PlugIn class functionality to develop extentions for xmpppy.
+Also provides Client and Component classes implementations as the
+examples of xmpppy structures usage.
+These classes can be used for simple applications "AS IS" though.
+"""
+
+from . import debug
+from . import transports
+from . import dispatcher
+from . import auth
+from . import roster
+
+from .plugin import PlugIn
+
+Debug = debug
+Debug.DEBUGGING_IS_ON = 1
+
+Debug.Debug.colors["socket"] = debug.color_dark_gray
+Debug.Debug.colors["CONNECTproxy"] = debug.color_dark_gray
+Debug.Debug.colors["nodebuilder"] = debug.color_brown
+Debug.Debug.colors["client"] = debug.color_cyan
+Debug.Debug.colors["component"] = debug.color_cyan
+Debug.Debug.colors["dispatcher"] = debug.color_green
+Debug.Debug.colors["browser"] = debug.color_blue
+Debug.Debug.colors["auth"] = debug.color_yellow
+Debug.Debug.colors["roster"] = debug.color_magenta
+Debug.Debug.colors["ibb"] = debug.color_yellow
+Debug.Debug.colors["down"] = debug.color_brown
+Debug.Debug.colors["up"] = debug.color_brown
+Debug.Debug.colors["data"] = debug.color_brown
+Debug.Debug.colors["ok"] = debug.color_green
+Debug.Debug.colors["warn"] = debug.color_yellow
+Debug.Debug.colors["error"] = debug.color_red
+Debug.Debug.colors["start"] = debug.color_dark_gray
+Debug.Debug.colors["stop"] = debug.color_dark_gray
+Debug.Debug.colors["sent"] = debug.color_yellow
+Debug.Debug.colors["got"] = debug.color_bright_cyan
+
+DBG_CLIENT = "client"
+DBG_COMPONENT = "component"
+
+
+class CommonClient:
+ """
+ Base for Client and Component classes.
+ """
+ def __init__(self, server, port=5222, debug=["always", "nodebuilder"]):
+ """
+ Caches server name and (optionally) port to connect to. "debug" parameter specifies
+ the debug IDs that will go into debug output. You can either specifiy an "include"
+ or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
+ Full list: ["nodebuilder", "dispatcher", "gen_auth", "SASL_auth", "bind", "socket",
+ "CONNECTproxy", "TLS", "roster", "browser", "ibb"].
+ """
+ if isinstance(self, Client):
+ self.Namespace, self.DBG = "jabber:client", DBG_CLIENT
+ elif isinstance(self, Component):
+ self.Namespace, self.DBG = dispatcher.NS_COMPONENT_ACCEPT, DBG_COMPONENT
+ self.defaultNamespace = self.Namespace
+ self.disconnect_handlers = []
+ self.Server = server
+ self.Port = port
+ if debug and not isinstance(debug, list):
+ debug = ["always", "nodebuilder"]
+ self._DEBUG = Debug.Debug(debug)
+ self.DEBUG = self._DEBUG.Show
+ self.debug_flags = self._DEBUG.debug_flags
+ self.debug_flags.append(self.DBG)
+ self._owner = self
+ self._registered_name = None
+ self.RegisterDisconnectHandler(self.DisconnectHandler)
+ self.connected = ""
+ self._route = 0
+
+ def RegisterDisconnectHandler(self, handler):
+ """
+ Register handler that will be called on disconnect.
+ """
+ self.disconnect_handlers.append(handler)
+
+ def UnregisterDisconnectHandler(self, handler):
+ """
+ Unregister handler that is called on disconnect.
+ """
+ self.disconnect_handlers.remove(handler)
+
+ def disconnected(self):
+ """
+ Called on disconnection. Calls disconnect handlers and cleans things up.
+ """
+ self.connected = ""
+ self.DEBUG(self.DBG, "Disconnect detected", "stop")
+ self.disconnect_handlers.reverse()
+ for dhnd in self.disconnect_handlers:
+ dhnd()
+ self.disconnect_handlers.reverse()
+ if hasattr(self, "TLS"):
+ self.TLS.PlugOut()
+
+ def DisconnectHandler(self):
+ """
+ Default disconnect handler. Just raises an IOError.
+ If you choosed to use this class in your production client,
+ override this method or at least unregister it.
+ """
+ raise IOError("Disconnected!")
+
+ def event(self, eventName, args={}):
+ """
+ Default event handler. To be overriden.
+ """
+ print("Event: %s-%s" % (eventName, args))
+
+ def isConnected(self):
+ """
+ Returns connection state. F.e.: None / "tls" / "tcp+non_sasl" .
+ """
+ return self.connected
+
+ def reconnectAndReauth(self, handlerssave=None):
+ """
+ Example of reconnection method. In fact, it can be used to batch connection and auth as well.
+ """
+ Dispatcher_ = False
+ if not handlerssave:
+ Dispatcher_, handlerssave = True, self.Dispatcher.dumpHandlers()
+ if hasattr(self, "ComponentBind"):
+ self.ComponentBind.PlugOut()
+ if hasattr(self, "Bind"):
+ self.Bind.PlugOut()
+ self._route = 0
+ if hasattr(self, "NonSASL"):
+ self.NonSASL.PlugOut()
+ if hasattr(self, "SASL"):
+ self.SASL.PlugOut()
+ if hasattr(self, "TLS"):
+ self.TLS.PlugOut()
+ if Dispatcher_:
+ self.Dispatcher.PlugOut()
+ if hasattr(self, "HTTPPROXYsocket"):
+ self.HTTPPROXYsocket.PlugOut()
+ if hasattr(self, "TCPsocket"):
+ self.TCPsocket.PlugOut()
+ if not self.connect(server=self._Server, proxy=self._Proxy):
+ return None
+ if not self.auth(self._User, self._Password, self._Resource):
+ return None
+ self.Dispatcher.restoreHandlers(handlerssave)
+ return self.connected
+
+ def connect(self, server=None, proxy=None, ssl=None, use_srv=False):
+ """
+ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
+ Returns None or "tcp" or "tls", depending on the result.
+ """
+ if not server:
+ server = (self.Server, self.Port)
+ if proxy:
+ sock = transports.HTTPPROXYsocket(proxy, server, use_srv)
+ else:
+ sock = transports.TCPsocket(server, use_srv)
+ connected = sock.PlugIn(self)
+ if not connected:
+ sock.PlugOut()
+ return None
+ self._Server, self._Proxy = server, proxy
+ self.connected = "tcp"
+ if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl:
+ try: # FIXME. This should be done in transports.py
+ transports.TLS().PlugIn(self, now=1)
+ self.connected = "ssl"
+ except transports.socket.sslerror:
+ return None
+ dispatcher.Dispatcher().PlugIn(self)
+ while self.Dispatcher.Stream._document_attrs is None:
+ if not self.Process(1):
+ return None
+ if "version" in self.Dispatcher.Stream._document_attrs and self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ return self.connected
+
+class Client(CommonClient):
+ """
+ Example client class, based on CommonClient.
+ """
+ def connect(self, server=None, proxy=None, secure=None, use_srv=True):
+ """
+ Connect to jabber server. If you want to specify different ip/port to connect to you can
+ pass it as tuple as first parameter. If there is HTTP proxy between you and server
+ specify it's address and credentials (if needed) in the second argument.
+ If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443)
+ If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1.
+ If you want to disable tls/ssl support completely, set it to 0.
+ Example: connect(("192.168.5.5", 5222), {"host": "proxy.my.net", "port": 8080, "user": "me", "password": "secret"})
+ Returns "" or "tcp" or "tls", depending on the result.
+ """
+ if not CommonClient.connect(self, server, proxy, secure, use_srv) or secure != None and not secure:
+ return self.connected
+ transports.TLS().PlugIn(self)
+ if not hasattr(self, "Dispatcher"):
+ return None
+ if "version" not in self.Dispatcher.Stream._document_attrs or not self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ return self.connected
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ if not self.Dispatcher.Stream.features.getTag("starttls"):
+ return self.connected # TLS not supported by server
+ while not self.TLS.starttls and self.Process(1):
+ pass
+ if not hasattr(self, "TLS") or self.TLS.starttls != "success":
+ self.event("tls_failed"); return self.connected
+ self.connected = "tls"
+ return self.connected
+
+ def auth(self, user, password, resource="", sasl=1):
+ """
+ Authenticate connnection and bind resource. If resource is not provided
+ random one or library name used.
+ """
+ self._User, self._Password, self._Resource = user, password, resource
+ while not self.Dispatcher.Stream._document_attrs and self.Process(1):
+ pass
+ if "version" in self.Dispatcher.Stream._document_attrs and self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ if sasl:
+ auth.SASL(user, password).PlugIn(self)
+ if not sasl or self.SASL.startsasl == "not-supported":
+ if not resource:
+ resource = "xmpppy"
+ if auth.NonSASL(user, password, resource).PlugIn(self):
+ self.connected += "+old_auth"
+ return "old_auth"
+ return None
+ self.SASL.auth()
+ while self.SASL.startsasl == "in-process" and self.Process(1):
+ pass
+ if self.SASL.startsasl == "success":
+ auth.Bind().PlugIn(self)
+ while self.Bind.bound is None and self.Process(1):
+ pass
+ if self.Bind.Bind(resource):
+ self.connected += "+sasl"
+ return "sasl"
+ elif hasattr(self, "SASL"):
+ self.SASL.PlugOut()
+
+ def getRoster(self):
+ """
+ Return the Roster instance, previously plugging it in and
+ requesting roster from server if needed.
+ """
+ if not hasattr(self, "Roster"):
+ roster.Roster().PlugIn(self)
+ return self.Roster.getRoster()
+
+ def sendInitPresence(self, requestRoster=1):
+ """
+ Send roster request and initial <presence/>.
+ You can disable the first by setting requestRoster argument to 0.
+ """
+ self.sendPresence(requestRoster=requestRoster)
+
+ def sendPresence(self, jid=None, typ=None, requestRoster=0):
+ """
+ Send some specific presence state.
+ Can also request roster from server if according agrument is set.
+ """
+ if requestRoster:
+ roster.Roster().PlugIn(self)
+ self.send(dispatcher.Presence(to=jid, typ=typ))
+
+class Component(CommonClient):
+ """
+ Component class. The only difference from CommonClient is ability to perform component authentication.
+ """
+ def __init__(self, transport, port=5347, typ=None, debug=["always", "nodebuilder"], domains=None, sasl=0, bind=0, route=0, xcp=0):
+ """
+ Init function for Components.
+ As components use a different auth mechanism which includes the namespace of the component.
+ Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
+ Jabberd2 uses jabber:client.
+ "transport" argument is a transport name that you are going to serve (f.e. "irc.localhost").
+ "port" can be specified if "transport" resolves to correct IP. If it is not then you'll have to specify IP
+ and port while calling "connect()".
+ If you are going to serve several different domains with single Component instance - you must list them ALL
+ in the "domains" argument.
+ For jabberd2 servers you should set typ="jabberd2" argument.
+ """
+ CommonClient.__init__(self, transport, port=port, debug=debug)
+ self.typ = typ
+ self.sasl = sasl
+ self.bind = bind
+ self.route = route
+ self.xcp = xcp
+ if domains:
+ self.domains = domains
+ else:
+ self.domains = [transport]
+
+ def connect(self, server=None, proxy=None):
+ """
+ This will connect to the server, and if the features tag is found then set
+ the namespace to be jabber:client as that is required for jabberd2.
+ "server" and "proxy" arguments have the same meaning as in xmpp.Client.connect().
+ """
+ if self.sasl:
+ self.Namespace = auth.NS_COMPONENT_1
+ self.Server = server[0]
+ CommonClient.connect(self, server=server, proxy=proxy)
+ if self.connected and (self.typ == "jabberd2" or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp):
+ self.defaultNamespace = auth.NS_CLIENT
+ self.Dispatcher.RegisterNamespace(self.defaultNamespace)
+ self.Dispatcher.RegisterProtocol("iq", dispatcher.Iq)
+ self.Dispatcher.RegisterProtocol("message", dispatcher.Message)
+ self.Dispatcher.RegisterProtocol("presence", dispatcher.Presence)
+ return self.connected
+
+ def dobind(self, sasl):
+ # This has to be done before binding, because we can receive a route stanza before binding finishes
+ self._route = self.route
+ if self.bind:
+ for domain in self.domains:
+ auth.ComponentBind(sasl).PlugIn(self)
+ while self.ComponentBind.bound is None:
+ self.Process(1)
+ if (not self.ComponentBind.Bind(domain)):
+ self.ComponentBind.PlugOut()
+ return None
+ self.ComponentBind.PlugOut()
+
+ def auth(self, name, password, dup=None):
+ """
+ Authenticate component "name" with password "password".
+ """
+ self._User, self._Password, self._Resource = name, password, ""
+ try:
+ if self.sasl:
+ auth.SASL(name, password).PlugIn(self)
+ if not self.sasl or self.SASL.startsasl == "not-supported":
+ if auth.NonSASL(name, password, "").PlugIn(self):
+ self.dobind(sasl=False)
+ self.connected += "+old_auth"
+ return "old_auth"
+ return None
+ self.SASL.auth()
+ while self.SASL.startsasl == "in-process" and self.Process(1):
+ pass
+ if self.SASL.startsasl == "success":
+ self.dobind(sasl=True)
+ self.connected += "+sasl"
+ return "sasl"
+ else:
+ raise auth.NotAuthorized(self.SASL.startsasl)
+ except Exception:
+ self.DEBUG(self.DBG, "Failed to authenticate %s" % name, "error")
diff --git a/xmpp/commands.py b/xmpp/commands.py index 3d4f75c..42489ab 100644 --- a/xmpp/commands.py +++ b/xmpp/commands.py @@ -1,448 +1,448 @@ -## Ad-Hoc Command manager - -## Mike Albon (c) 5th January 2005 - -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: commands.py, v1.18 2013/11/05 alkorgun Exp $ - -""" -This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. -It depends on a DISCO browser manager. - -There are 3 classes here, a command processor Commands like the Browser, -and a command template plugin Command, and an example command. - -To use this module: - - Instansiate the module with the parent transport and disco browser manager as parameters. - "Plug in" commands using the command template. - The command feature must be added to existing disco replies where neccessary. - -What it supplies: - - Automatic command registration with the disco browser manager. - Automatic listing of commands in the public command list. - A means of handling requests, by redirection though the command manager. -""" - -from plugin import PlugIn -from protocol import * - -class Commands(PlugIn): - """ - Commands is an ancestor of PlugIn and can be attached to any session. - - The commands class provides a lookup and browse mechnism. - It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, - it adds the "list" disco type to your existing disco handler function. - - How it works: - The commands are added into the existing Browser on the correct nodes. - When the command list is built the supplied discovery handler function needs to have a "list" option in type. - This then gets enumerated, all results returned as None are ignored. - The command executed is then called using it's Execute method. - All session management is handled by the command itself. - """ - def __init__(self, browser): - """ - Initialises class and sets up local variables. - """ - PlugIn.__init__(self) - DBG_LINE = "commands" - self._exported_methods = [] - self._handlers = {"": {}} - self._browser = browser - - def plugin(self, owner): - """ - Makes handlers within the session. - """ - # Plug into the session and the disco manager - # We only need get and set, results are not needed by a service provider, only a service user. - owner.RegisterHandler("iq", self._CommandHandler, typ="set", ns=NS_COMMANDS) - owner.RegisterHandler("iq", self._CommandHandler, typ="get", ns=NS_COMMANDS) - self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid="") - - def plugout(self): - """ - Removes handlers from the session. - """ - # unPlug from the session and the disco manager - self._owner.UnregisterHandler("iq", self._CommandHandler, ns=NS_COMMANDS) - for jid in self._handlers: - self._browser.delDiscoHandler(self._DiscoHandler, node=NS_COMMANDS) - - def _CommandHandler(self, conn, request): - """ - The internal method to process the routing of command execution requests. - """ - # This is the command handler itself. - # We must: - # Pass on command execution to command handler - # (Do we need to keep session details here, or can that be done in the command?) - jid = str(request.getTo()) - try: - node = request.getTagAttr("command", "node") - except Exception: - conn.send(Error(request, ERR_BAD_REQUEST)) - raise NodeProcessed() - if jid in self._handlers: - if node in self._handlers[jid]: - self._handlers[jid][node]["execute"](conn, request) - else: - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - elif node in self._handlers[""]: - self._handlers[""][node]["execute"](conn, request) - else: - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - - def _DiscoHandler(self, conn, request, typ): - """ - The internal method to process service discovery requests. - """ - # This is the disco manager handler. - if typ == "items": - # We must: - # Generate a list of commands and return the list - # * This handler does not handle individual commands disco requests. - # Pseudo: - # Enumerate the "item" disco of each command for the specified jid - # Build responce and send - # To make this code easy to write we add an "list" disco type, it returns a tuple or "none" if not advertised - list = [] - items = [] - jid = str(request.getTo()) - # Get specific jid based results - if jid in self._handlers: - for each in self._handlers[jid].keys(): - items.append((jid, each)) - else: - # Get generic results - for each in self._handlers[""].keys(): - items.append(("", each)) - if items: - for each in items: - i = self._handlers[each[0]][each[1]]["disco"](conn, request, "list") - if i != None: - list.append(Node(tag="item", attrs={"jid": i[0], "node": i[1], "name": i[2]})) - iq = request.buildReply("result") - if request.getQuerynode(): - iq.setQuerynode(request.getQuerynode()) - iq.setQueryPayload(list) - conn.send(iq) - else: - conn.send(Error(request, ERR_ITEM_NOT_FOUND)) - raise NodeProcessed() - if typ == "info": - return { - "ids": [{"category": "automation", "type": "command-list"}], - "features": [] - } - - def addCommand(self, name, cmddisco, cmdexecute, jid=""): - """ - The method to call if adding a new command to the session, - the requred parameters of cmddisco and cmdexecute - are the methods to enable that command to be executed. - """ - # This command takes a command object and the name of the command for registration - # We must: - # Add item into disco - # Add item into command list - if jid not in self._handlers: - self._handlers[jid] = {} - self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid=jid) - if name in self._handlers[jid]: - raise NameError("Command Exists") - self._handlers[jid][name] = {"disco": cmddisco, "execute": cmdexecute} - # Need to add disco stuff here - self._browser.setDiscoHandler(cmddisco, node=name, jid=jid) - - def delCommand(self, name, jid=""): - """ - Removed command from the session. - """ - # This command takes a command object and the name used for registration - # We must: - # Remove item from disco - # Remove item from command list - if jid not in self._handlers: - raise NameError("Jid not found") - if name not in self._handlers[jid]: - raise NameError("Command not found") - # Do disco removal here - command = self.getCommand(name, jid)["disco"] - del self._handlers[jid][name] - self._browser.delDiscoHandler(command, node=name, jid=jid) - - def getCommand(self, name, jid=""): - """ - Returns the command tuple. - """ - # This gets the command object with name - # We must: - # Return item that matches this name - if jid not in self._handlers: - raise NameError("Jid not found") - if name not in self._handlers[jid]: - raise NameError("Command not found") - return self._handlers[jid][name] - -class Command_Handler_Prototype(PlugIn): - """ - This is a prototype command handler, as each command uses a disco method - and execute method you can implement it any way you like, however this is - my first attempt at making a generic handler that you can hang process - stages on too. There is an example command below. - - The parameters are as follows: - name: the name of the command within the jabber environment - description: the natural language description - discofeatures: the features supported by the command - initial: the initial command in the from of {"execute": commandname} - - All stages set the "actions" dictionary for each session to represent the possible options available. - """ - name = "examplecommand" - count = 0 - description = "an example command" - discofeatures = [NS_COMMANDS, NS_DATA] - - # This is the command template - def __init__(self, jid=""): - """ - Set up the class. - """ - PlugIn.__init__(self) - DBG_LINE = "command" - self.sessioncount = 0 - self.sessions = {} - # Disco information for command list pre-formatted as a tuple - self.discoinfo = { - "ids": [{ - "category": "automation", - "type": "command-node", - "name": self.description - }], - "features": self.discofeatures - } - self._jid = jid - - def plugin(self, owner): - """ - Plug command into the commands class. - """ - # The owner in this instance is the Command Processor - self._commands = owner - self._owner = owner._owner - self._commands.addCommand(self.name, self._DiscoHandler, self.Execute, jid=self._jid) - - def plugout(self): - """ - Remove command from the commands class. - """ - self._commands.delCommand(self.name, self._jid) - - def getSessionID(self): - """ - Returns an id for the command session. - """ - self.count += 1 - return "cmd-%s-%d" % (self.name, self.count) - - def Execute(self, conn, request): - """ - The method that handles all the commands, and routes them to the correct method for that stage. - """ - # New request or old? - try: - session = request.getTagAttr("command", "sessionid") - except Exception: - session = None - try: - action = request.getTagAttr("command", "action") - except Exception: - action = None - if action == None: - action = "execute" - # Check session is in session list - if session in self.sessions: - if self.sessions[session]["jid"] == request.getFrom(): - # Check action is vaild - if action in self.sessions[session]["actions"]: - # Execute next action - self.sessions[session]["actions"][action](conn, request) - else: - # Stage not presented as an option - self._owner.send(Error(request, ERR_BAD_REQUEST)) - raise NodeProcessed() - else: - # Jid and session don't match. Go away imposter - self._owner.send(Error(request, ERR_BAD_REQUEST)) - raise NodeProcessed() - elif session != None: - # Not on this sessionid you won't. - self._owner.send(Error(request, ERR_BAD_REQUEST)) - raise NodeProcessed() - else: - # New session - self.initial[action](conn, request) - - def _DiscoHandler(self, conn, request, typ): - """ - The handler for discovery events. - """ - if typ == "list": - result = (request.getTo(), self.name, self.description) - elif typ == "items": - result = [] - elif typ == "info": - result = self.discoinfo - return result - -class TestCommand(Command_Handler_Prototype): - """ - Example class. You should read source if you wish to understate how it works. - Generally, it presents a "master" that giudes user through to calculate something. - """ - name = "testcommand" - description = "a noddy example command" - - def __init__(self, jid=""): - """ Init internal constants. """ - Command_Handler_Prototype.__init__(self, jid) - self.initial = {"execute": self.cmdFirstStage} - - def cmdFirstStage(self, conn, request): - """ - Determine. - """ - # This is the only place this should be repeated as all other stages should have SessionIDs - try: - session = request.getTagAttr("command", "sessionid") - except Exception: - session = None - if session == None: - session = self.getSessionID() - self.sessions[session] = { - "jid": request.getFrom(), - "actions": { - "cancel": self.cmdCancel, - "next": self.cmdSecondStage, - "execute": self.cmdSecondStage - }, - "data": {"type": None} - } - # As this is the first stage we only send a form - reply = request.buildReply("result") - form = DataForm(title="Select type of operation", - data=[ - "Use the combobox to select the type of calculation you would like to do, then click Next.", - DataField(name="calctype", desc="Calculation Type", - value=self.sessions[session]["data"]["type"], - options=[ - ["circlediameter", "Calculate the Diameter of a circle"], - ["circlearea", "Calculate the area of a circle"] - ], - typ="list-single", - required=1 - )]) - replypayload = [Node("actions", attrs={"execute": "next"}, payload=[Node("next")]), form] - reply.addChild(name="command", - namespace=NS_COMMANDS, - attrs={ - "node": request.getTagAttr("command", "node"), - "sessionid": session, - "status": "executing" - }, - payload=replypayload - ) - self._owner.send(reply) - raise NodeProcessed() - - def cmdSecondStage(self, conn, request): - form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA)) - self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] = form.getField("calctype").getValue() - self.sessions[request.getTagAttr("command", "sessionid")]["actions"] = { - "cancel": self.cmdCancel, - None: self.cmdThirdStage, - "previous": self.cmdFirstStage, - "execute": self.cmdThirdStage, - "next": self.cmdThirdStage - } - # The form generation is split out to another method as it may be called by cmdThirdStage - self.cmdSecondStageReply(conn, request) - - def cmdSecondStageReply(self, conn, request): - reply = request.buildReply("result") - form = DataForm(title="Enter the radius", - data=[ - "Enter the radius of the circle (numbers only)", - DataField(desc="Radius", name="radius", typ="text-single") - ]) - replypayload = [ - Node("actions", - attrs={"execute": "complete"}, - payload=[Node("complete"), - Node("prev")]), - form - ] - reply.addChild(name="command", - namespace=NS_COMMANDS, - attrs={ - "node": request.getTagAttr("command", "node"), - "sessionid": request.getTagAttr("command", "sessionid"), - "status": "executing" - }, - payload=replypayload - ) - self._owner.send(reply) - raise NodeProcessed() - - def cmdThirdStage(self, conn, request): - form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA)) - try: - numb = float(form.getField("radius").getValue()) - except Exception: - self.cmdSecondStageReply(conn, request) - from math import pi - if self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] == "circlearea": - result = (numb ** 2) * pi - else: - result = numb * 2 * pi - reply = request.buildReply("result") - form = DataForm(typ="result", data=[DataField(desc="result", name="result", value=result)]) - reply.addChild(name="command", - namespace=NS_COMMANDS, - attrs={ - "node": request.getTagAttr("command", "node"), - "sessionid": request.getTagAttr("command", "sessionid"), - "status": "completed" - }, - payload=[form] - ) - self._owner.send(reply) - raise NodeProcessed() - - def cmdCancel(self, conn, request): - reply = request.buildReply("result") - reply.addChild(name="command", - namespace=NS_COMMANDS, - attrs={ - "node": request.getTagAttr("command", "node"), - "sessionid": request.getTagAttr("command", "sessionid"), - "status": "cancelled" - }) - self._owner.send(reply) - del self.sessions[request.getTagAttr("command", "sessionid")] +## Ad-Hoc Command manager
+
+## Mike Albon (c) 5th January 2005
+
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: commands.py, v1.18 2013/11/05 alkorgun Exp $
+
+"""
+This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library.
+It depends on a DISCO browser manager.
+
+There are 3 classes here, a command processor Commands like the Browser,
+and a command template plugin Command, and an example command.
+
+To use this module:
+
+ Instansiate the module with the parent transport and disco browser manager as parameters.
+ "Plug in" commands using the command template.
+ The command feature must be added to existing disco replies where neccessary.
+
+What it supplies:
+
+ Automatic command registration with the disco browser manager.
+ Automatic listing of commands in the public command list.
+ A means of handling requests, by redirection though the command manager.
+"""
+
+from .plugin import PlugIn
+from .protocol import *
+
+class Commands(PlugIn):
+ """
+ Commands is an ancestor of PlugIn and can be attached to any session.
+
+ The commands class provides a lookup and browse mechnism.
+ It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands,
+ it adds the "list" disco type to your existing disco handler function.
+
+ How it works:
+ The commands are added into the existing Browser on the correct nodes.
+ When the command list is built the supplied discovery handler function needs to have a "list" option in type.
+ This then gets enumerated, all results returned as None are ignored.
+ The command executed is then called using it's Execute method.
+ All session management is handled by the command itself.
+ """
+ def __init__(self, browser):
+ """
+ Initialises class and sets up local variables.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "commands"
+ self._exported_methods = []
+ self._handlers = {"": {}}
+ self._browser = browser
+
+ def plugin(self, owner):
+ """
+ Makes handlers within the session.
+ """
+ # Plug into the session and the disco manager
+ # We only need get and set, results are not needed by a service provider, only a service user.
+ owner.RegisterHandler("iq", self._CommandHandler, typ="set", ns=NS_COMMANDS)
+ owner.RegisterHandler("iq", self._CommandHandler, typ="get", ns=NS_COMMANDS)
+ self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid="")
+
+ def plugout(self):
+ """
+ Removes handlers from the session.
+ """
+ # unPlug from the session and the disco manager
+ self._owner.UnregisterHandler("iq", self._CommandHandler, ns=NS_COMMANDS)
+ for jid in self._handlers:
+ self._browser.delDiscoHandler(self._DiscoHandler, node=NS_COMMANDS)
+
+ def _CommandHandler(self, conn, request):
+ """
+ The internal method to process the routing of command execution requests.
+ """
+ # This is the command handler itself.
+ # We must:
+ # Pass on command execution to command handler
+ # (Do we need to keep session details here, or can that be done in the command?)
+ jid = str(request.getTo())
+ try:
+ node = request.getTagAttr("command", "node")
+ except Exception:
+ conn.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ if jid in self._handlers:
+ if node in self._handlers[jid]:
+ self._handlers[jid][node]["execute"](conn, request)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ elif node in self._handlers[""]:
+ self._handlers[""][node]["execute"](conn, request)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+
+ def _DiscoHandler(self, conn, request, typ):
+ """
+ The internal method to process service discovery requests.
+ """
+ # This is the disco manager handler.
+ if typ == "items":
+ # We must:
+ # Generate a list of commands and return the list
+ # * This handler does not handle individual commands disco requests.
+ # Pseudo:
+ # Enumerate the "item" disco of each command for the specified jid
+ # Build responce and send
+ # To make this code easy to write we add an "list" disco type, it returns a tuple or "none" if not advertised
+ list = []
+ items = []
+ jid = str(request.getTo())
+ # Get specific jid based results
+ if jid in self._handlers:
+ for each in self._handlers[jid].keys():
+ items.append((jid, each))
+ else:
+ # Get generic results
+ for each in self._handlers[""].keys():
+ items.append(("", each))
+ if items:
+ for each in items:
+ i = self._handlers[each[0]][each[1]]["disco"](conn, request, "list")
+ if i != None:
+ list.append(Node(tag="item", attrs={"jid": i[0], "node": i[1], "name": i[2]}))
+ iq = request.buildReply("result")
+ if request.getQuerynode():
+ iq.setQuerynode(request.getQuerynode())
+ iq.setQueryPayload(list)
+ conn.send(iq)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ if typ == "info":
+ return {
+ "ids": [{"category": "automation", "type": "command-list"}],
+ "features": []
+ }
+
+ def addCommand(self, name, cmddisco, cmdexecute, jid=""):
+ """
+ The method to call if adding a new command to the session,
+ the requred parameters of cmddisco and cmdexecute
+ are the methods to enable that command to be executed.
+ """
+ # This command takes a command object and the name of the command for registration
+ # We must:
+ # Add item into disco
+ # Add item into command list
+ if jid not in self._handlers:
+ self._handlers[jid] = {}
+ self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid=jid)
+ if name in self._handlers[jid]:
+ raise NameError("Command Exists")
+ self._handlers[jid][name] = {"disco": cmddisco, "execute": cmdexecute}
+ # Need to add disco stuff here
+ self._browser.setDiscoHandler(cmddisco, node=name, jid=jid)
+
+ def delCommand(self, name, jid=""):
+ """
+ Removed command from the session.
+ """
+ # This command takes a command object and the name used for registration
+ # We must:
+ # Remove item from disco
+ # Remove item from command list
+ if jid not in self._handlers:
+ raise NameError("Jid not found")
+ if name not in self._handlers[jid]:
+ raise NameError("Command not found")
+ # Do disco removal here
+ command = self.getCommand(name, jid)["disco"]
+ del self._handlers[jid][name]
+ self._browser.delDiscoHandler(command, node=name, jid=jid)
+
+ def getCommand(self, name, jid=""):
+ """
+ Returns the command tuple.
+ """
+ # This gets the command object with name
+ # We must:
+ # Return item that matches this name
+ if jid not in self._handlers:
+ raise NameError("Jid not found")
+ if name not in self._handlers[jid]:
+ raise NameError("Command not found")
+ return self._handlers[jid][name]
+
+class Command_Handler_Prototype(PlugIn):
+ """
+ This is a prototype command handler, as each command uses a disco method
+ and execute method you can implement it any way you like, however this is
+ my first attempt at making a generic handler that you can hang process
+ stages on too. There is an example command below.
+
+ The parameters are as follows:
+ name: the name of the command within the jabber environment
+ description: the natural language description
+ discofeatures: the features supported by the command
+ initial: the initial command in the from of {"execute": commandname}
+
+ All stages set the "actions" dictionary for each session to represent the possible options available.
+ """
+ name = "examplecommand"
+ count = 0
+ description = "an example command"
+ discofeatures = [NS_COMMANDS, NS_DATA]
+
+ # This is the command template
+ def __init__(self, jid=""):
+ """
+ Set up the class.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "command"
+ self.sessioncount = 0
+ self.sessions = {}
+ # Disco information for command list pre-formatted as a tuple
+ self.discoinfo = {
+ "ids": [{
+ "category": "automation",
+ "type": "command-node",
+ "name": self.description
+ }],
+ "features": self.discofeatures
+ }
+ self._jid = jid
+
+ def plugin(self, owner):
+ """
+ Plug command into the commands class.
+ """
+ # The owner in this instance is the Command Processor
+ self._commands = owner
+ self._owner = owner._owner
+ self._commands.addCommand(self.name, self._DiscoHandler, self.Execute, jid=self._jid)
+
+ def plugout(self):
+ """
+ Remove command from the commands class.
+ """
+ self._commands.delCommand(self.name, self._jid)
+
+ def getSessionID(self):
+ """
+ Returns an id for the command session.
+ """
+ self.count += 1
+ return "cmd-%s-%d" % (self.name, self.count)
+
+ def Execute(self, conn, request):
+ """
+ The method that handles all the commands, and routes them to the correct method for that stage.
+ """
+ # New request or old?
+ try:
+ session = request.getTagAttr("command", "sessionid")
+ except Exception:
+ session = None
+ try:
+ action = request.getTagAttr("command", "action")
+ except Exception:
+ action = None
+ if action == None:
+ action = "execute"
+ # Check session is in session list
+ if session in self.sessions:
+ if self.sessions[session]["jid"] == request.getFrom():
+ # Check action is vaild
+ if action in self.sessions[session]["actions"]:
+ # Execute next action
+ self.sessions[session]["actions"][action](conn, request)
+ else:
+ # Stage not presented as an option
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ else:
+ # Jid and session don't match. Go away imposter
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ elif session != None:
+ # Not on this sessionid you won't.
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ else:
+ # New session
+ self.initial[action](conn, request)
+
+ def _DiscoHandler(self, conn, request, typ):
+ """
+ The handler for discovery events.
+ """
+ if typ == "list":
+ result = (request.getTo(), self.name, self.description)
+ elif typ == "items":
+ result = []
+ elif typ == "info":
+ result = self.discoinfo
+ return result
+
+class TestCommand(Command_Handler_Prototype):
+ """
+ Example class. You should read source if you wish to understate how it works.
+ Generally, it presents a "master" that giudes user through to calculate something.
+ """
+ name = "testcommand"
+ description = "a noddy example command"
+
+ def __init__(self, jid=""):
+ """ Init internal constants. """
+ Command_Handler_Prototype.__init__(self, jid)
+ self.initial = {"execute": self.cmdFirstStage}
+
+ def cmdFirstStage(self, conn, request):
+ """
+ Determine.
+ """
+ # This is the only place this should be repeated as all other stages should have SessionIDs
+ try:
+ session = request.getTagAttr("command", "sessionid")
+ except Exception:
+ session = None
+ if session == None:
+ session = self.getSessionID()
+ self.sessions[session] = {
+ "jid": request.getFrom(),
+ "actions": {
+ "cancel": self.cmdCancel,
+ "next": self.cmdSecondStage,
+ "execute": self.cmdSecondStage
+ },
+ "data": {"type": None}
+ }
+ # As this is the first stage we only send a form
+ reply = request.buildReply("result")
+ form = DataForm(title="Select type of operation",
+ data=[
+ "Use the combobox to select the type of calculation you would like to do, then click Next.",
+ DataField(name="calctype", desc="Calculation Type",
+ value=self.sessions[session]["data"]["type"],
+ options=[
+ ["circlediameter", "Calculate the Diameter of a circle"],
+ ["circlearea", "Calculate the area of a circle"]
+ ],
+ typ="list-single",
+ required=1
+ )])
+ replypayload = [Node("actions", attrs={"execute": "next"}, payload=[Node("next")]), form]
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": session,
+ "status": "executing"
+ },
+ payload=replypayload
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdSecondStage(self, conn, request):
+ form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA))
+ self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] = form.getField("calctype").getValue()
+ self.sessions[request.getTagAttr("command", "sessionid")]["actions"] = {
+ "cancel": self.cmdCancel,
+ None: self.cmdThirdStage,
+ "previous": self.cmdFirstStage,
+ "execute": self.cmdThirdStage,
+ "next": self.cmdThirdStage
+ }
+ # The form generation is split out to another method as it may be called by cmdThirdStage
+ self.cmdSecondStageReply(conn, request)
+
+ def cmdSecondStageReply(self, conn, request):
+ reply = request.buildReply("result")
+ form = DataForm(title="Enter the radius",
+ data=[
+ "Enter the radius of the circle (numbers only)",
+ DataField(desc="Radius", name="radius", typ="text-single")
+ ])
+ replypayload = [
+ Node("actions",
+ attrs={"execute": "complete"},
+ payload=[Node("complete"),
+ Node("prev")]),
+ form
+ ]
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "executing"
+ },
+ payload=replypayload
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdThirdStage(self, conn, request):
+ form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA))
+ try:
+ numb = float(form.getField("radius").getValue())
+ except Exception:
+ self.cmdSecondStageReply(conn, request)
+ from math import pi
+ if self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] == "circlearea":
+ result = (numb ** 2) * pi
+ else:
+ result = numb * 2 * pi
+ reply = request.buildReply("result")
+ form = DataForm(typ="result", data=[DataField(desc="result", name="result", value=result)])
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "completed"
+ },
+ payload=[form]
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdCancel(self, conn, request):
+ reply = request.buildReply("result")
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "cancelled"
+ })
+ self._owner.send(reply)
+ del self.sessions[request.getTagAttr("command", "sessionid")]
diff --git a/xmpp/debug.py b/xmpp/debug.py index d4c33ce..6d8c34a 100644 --- a/xmpp/debug.py +++ b/xmpp/debug.py @@ -1,314 +1,314 @@ -## debug.py -## -## Copyright (C) 2003 Jacob Lundqvist -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published -## by the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. - -# $Id: debug.py, v1.41 2013/10/21 alkorgun Exp $ - -_version_ = "1.4.1" - -import os -import sys -import time - -from traceback import format_exception as traceback_format_exception - -colors_enabled = os.environ.has_key("TERM") - -color_none = chr(27) + "[0m" -color_black = chr(27) + "[30m" -color_red = chr(27) + "[31m" -color_green = chr(27) + "[32m" -color_brown = chr(27) + "[33m" -color_blue = chr(27) + "[34m" -color_magenta = chr(27) + "[35m" -color_cyan = chr(27) + "[36m" -color_light_gray = chr(27) + "[37m" -color_dark_gray = chr(27) + "[30;1m" -color_bright_red = chr(27) + "[31;1m" -color_bright_green = chr(27) + "[32;1m" -color_yellow = chr(27) + "[33;1m" -color_bright_blue = chr(27) + "[34;1m" -color_purple = chr(27) + "[35;1m" -color_bright_cyan = chr(27) + "[36;1m" -color_white = chr(27) + "[37;1m" - -class NoDebug: - - def __init__(self, *args, **kwargs): - self.debug_flags = [] - - def show(self, *args, **kwargs): - pass - - def Show(self, *args, **kwargs): - pass - - def is_active(self, flag): - pass - - colors = {} - - def active_set(self, active_flags=None): - return 0 - -LINE_FEED = "\n" - -class Debug: - - def __init__(self, active_flags=None, log_file=sys.stderr, prefix="DEBUG: ", sufix="\n", time_stamp=0, flag_show=None, validate_flags=False, welcome=-1): - self.debug_flags = [] - if welcome == -1: - if active_flags and len(active_flags): - welcome = 1 - else: - welcome = 0 - self._remove_dupe_flags() - if log_file: - if isinstance(log_file, str): - try: - self._fh = open(log_file, "w") - except Exception: - print("ERROR: can open %s for writing." % log_file) - sys.exit(0) - else: # assume its a stream type object - self._fh = log_file - else: - self._fh = sys.stdout - if time_stamp not in (0, 1, 2): - raise Exception("Invalid time_stamp param", str(time_stamp)) - self.prefix = prefix - self.sufix = sufix - self.time_stamp = time_stamp - self.flag_show = None # must be initialised after possible welcome - self.validate_flags = validate_flags - self.active_set(active_flags) - if welcome: - self.show("") - caller = sys._getframe(1) # used to get name of caller - try: - mod_name = ":%s" % caller.f_locals["__name__"] - except Exception: - mod_name = "" - self.show("Debug created for %s%s" % (caller.f_code.co_filename, mod_name)) - self.show(" flags defined: %s" % ",".join(self.active)) - if isinstance(flag_show, (str, type(None))): - self.flag_show = flag_show - else: - raise Exception("Invalid type for flag_show!", str(flag_show)) - - def show(self, msg, flag=None, prefix=None, sufix=None, lf=0): - """ - flag can be of folowing types: - None - this msg will always be shown if any debugging is on - flag - will be shown if flag is active - (flag1,flag2,,,) - will be shown if any of the given flags are active - - if prefix / sufix are not given, default ones from init will be used - - lf = -1 means strip linefeed if pressent - lf = 1 means add linefeed if not pressent - """ - if self.validate_flags: - self._validate_flag(flag) - if not self.is_active(flag): - return None - if prefix: - pre = prefix - else: - pre = self.prefix - if sufix: - suf = sufix - else: - suf = self.sufix - if self.time_stamp == 2: - output = "%s%s " % ( - pre, - time.strftime("%b %d %H:%M:%S", - time.localtime(time.time())) - ) - elif self.time_stamp == 1: - output = "%s %s" % ( - time.strftime("%b %d %H:%M:%S", - time.localtime(time.time())), - pre - ) - else: - output = pre - if self.flag_show: - if flag: - output = "%s%s%s" % (output, flag, self.flag_show) - else: - # this call uses the global default, dont print "None", just show the separator - output = "%s %s" % (output, self.flag_show) - output = "%s%s%s" % (output, msg, suf) - if lf: - # strip/add lf if needed - last_char = output[-1] - if lf == 1 and last_char != LINE_FEED: - output = output + LINE_FEED - elif lf == -1 and last_char == LINE_FEED: - output = output[:-1] - try: - self._fh.write(output) - except Exception: - # unicode strikes again ;) - s = unicode() - for i in xrange(len(output)): - if ord(output[i]) < 128: - c = output[i] - else: - c = "?" - s += c - self._fh.write("%s%s%s" % (pre, s, suf)) - self._fh.flush() - - def is_active(self, flag): - """ - If given flag(s) should generate output. - """ - # try to abort early to quicken code - if not self.active: - return 0 - if not flag or flag in self.active: - return 1 - else: - # check for multi flag type: - if isinstance(flag, (list, tuple)): - for s in flag: - if s in self.active: - return 1 - return 0 - - def active_set(self, active_flags=None): - """ - Returns 1 if any flags where actually set, otherwise 0. - """ - r = 0 - ok_flags = [] - if not active_flags: - # no debuging at all - self.active = [] - elif isinstance(active_flags, (tuple, list)): - flags = self._as_one_list(active_flags) - for t in flags: - if t not in self.debug_flags: - sys.stderr.write("Invalid debugflag given: %s\n" % t) - ok_flags.append(t) - - self.active = ok_flags - r = 1 - else: - # assume comma string - try: - flags = active_flags.split(",") - except Exception: - self.show("***") - self.show("*** Invalid debug param given: %s" % active_flags) - self.show("*** please correct your param!") - self.show("*** due to this, full debuging is enabled") - self.active = self.debug_flags - for f in flags: - s = f.strip() - ok_flags.append(s) - self.active = ok_flags - self._remove_dupe_flags() - return r - - def active_get(self): - """ - Returns currently active flags. - """ - return self.active - - def _as_one_list(self, items): - """ - Init param might contain nested lists, typically from group flags. - This code organises lst and remves dupes. - """ - if not isinstance(items, (list, tuple)): - return [items] - r = [] - for l in items: - if isinstance(l, list): - lst2 = self._as_one_list(l) - for l2 in lst2: - self._append_unique_str(r, l2) - elif l == None: - continue - else: - self._append_unique_str(r, l) - return r - - def _append_unique_str(self, lst, item): - """ - Filter out any dupes. - """ - if not isinstance(item, str): - raise Exception("Invalid item type (should be string)", str(item)) - if item not in lst: - lst.append(item) - return lst - - def _validate_flag(self, flags): - """ - Verify that flag is defined. - """ - if flags: - for flag in self._as_one_list(flags): - if not flag in self.debug_flags: - raise Exception("Invalid debugflag given", str(flag)) - - def _remove_dupe_flags(self): - """ - If multiple instances of Debug is used in same app, - some flags might be created multiple time, filter out dupes. - """ - unique_flags = [] - for f in self.debug_flags: - if f not in unique_flags: - unique_flags.append(f) - self.debug_flags = unique_flags - - colors = {} - - def Show(self, flag, msg, prefix=""): - msg = msg.replace("\r", "\\r").replace("\n", "\\n").replace("><", ">\n <") - if not colors_enabled: - pass - elif self.colors.has_key(prefix): - msg = self.colors[prefix] + msg + color_none - else: - msg = color_none + msg - if not colors_enabled: - prefixcolor = "" - elif self.colors.has_key(flag): - prefixcolor = self.colors[flag] - else: - prefixcolor = color_none - if prefix == "error": - e = sys.exc_info() - if e[0]: - msg = msg + "\n" + "".join(traceback_format_exception(e[0], e[1], e[2])).rstrip() - prefix = self.prefix + prefixcolor + (flag + " " * 12)[:12] + " " + (prefix + " " * 6)[:6] - self.show(msg, flag, prefix) - - def is_active(self, flag): - if not self.active: - return 0 - if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active: - return 1 - return 0 - -DBG_ALWAYS = "always" - -# Debug=NoDebug # Uncomment this to effectively disable all debugging and all debugging overhead. +## debug.py
+##
+## Copyright (C) 2003 Jacob Lundqvist
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU Lesser General Public License as published
+## by the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU Lesser General Public License for more details.
+
+# $Id: debug.py, v1.41 2013/10/21 alkorgun Exp $
+
+_version_ = "1.4.1"
+
+import os
+import sys
+import time
+
+from traceback import format_exception as traceback_format_exception
+
+colors_enabled = "TERM" in os.environ
+
+color_none = chr(27) + "[0m"
+color_black = chr(27) + "[30m"
+color_red = chr(27) + "[31m"
+color_green = chr(27) + "[32m"
+color_brown = chr(27) + "[33m"
+color_blue = chr(27) + "[34m"
+color_magenta = chr(27) + "[35m"
+color_cyan = chr(27) + "[36m"
+color_light_gray = chr(27) + "[37m"
+color_dark_gray = chr(27) + "[30;1m"
+color_bright_red = chr(27) + "[31;1m"
+color_bright_green = chr(27) + "[32;1m"
+color_yellow = chr(27) + "[33;1m"
+color_bright_blue = chr(27) + "[34;1m"
+color_purple = chr(27) + "[35;1m"
+color_bright_cyan = chr(27) + "[36;1m"
+color_white = chr(27) + "[37;1m"
+
+class NoDebug:
+
+ def __init__(self, *args, **kwargs):
+ self.debug_flags = []
+
+ def show(self, *args, **kwargs):
+ pass
+
+ def Show(self, *args, **kwargs):
+ pass
+
+ def is_active(self, flag):
+ pass
+
+ colors = {}
+
+ def active_set(self, active_flags=None):
+ return 0
+
+LINE_FEED = "\n"
+
+class Debug:
+
+ def __init__(self, active_flags=None, log_file=sys.stderr, prefix="DEBUG: ", sufix="\n", time_stamp=0, flag_show=None, validate_flags=False, welcome=-1):
+ self.debug_flags = []
+ if welcome == -1:
+ if active_flags and len(active_flags):
+ welcome = 1
+ else:
+ welcome = 0
+ self._remove_dupe_flags()
+ if log_file:
+ if isinstance(log_file, str):
+ try:
+ self._fh = open(log_file, "w")
+ except Exception:
+ print("ERROR: can open %s for writing." % log_file)
+ sys.exit(0)
+ else: # assume its a stream type object
+ self._fh = log_file
+ else:
+ self._fh = sys.stdout
+ if time_stamp not in (0, 1, 2):
+ raise Exception("Invalid time_stamp param", str(time_stamp))
+ self.prefix = prefix
+ self.sufix = sufix
+ self.time_stamp = time_stamp
+ self.flag_show = None # must be initialised after possible welcome
+ self.validate_flags = validate_flags
+ self.active_set(active_flags)
+ if welcome:
+ self.show("")
+ caller = sys._getframe(1) # used to get name of caller
+ try:
+ mod_name = ":%s" % caller.f_locals["__name__"]
+ except Exception:
+ mod_name = ""
+ self.show("Debug created for %s%s" % (caller.f_code.co_filename, mod_name))
+ self.show(" flags defined: %s" % ",".join(self.active))
+ if isinstance(flag_show, (str, type(None))):
+ self.flag_show = flag_show
+ else:
+ raise Exception("Invalid type for flag_show!", str(flag_show))
+
+ def show(self, msg, flag=None, prefix=None, sufix=None, lf=0):
+ """
+ flag can be of folowing types:
+ None - this msg will always be shown if any debugging is on
+ flag - will be shown if flag is active
+ (flag1,flag2,,,) - will be shown if any of the given flags are active
+
+ if prefix / sufix are not given, default ones from init will be used
+
+ lf = -1 means strip linefeed if pressent
+ lf = 1 means add linefeed if not pressent
+ """
+ if self.validate_flags:
+ self._validate_flag(flag)
+ if not self.is_active(flag):
+ return None
+ if prefix:
+ pre = prefix
+ else:
+ pre = self.prefix
+ if sufix:
+ suf = sufix
+ else:
+ suf = self.sufix
+ if self.time_stamp == 2:
+ output = "%s%s " % (
+ pre,
+ time.strftime("%b %d %H:%M:%S",
+ time.localtime(time.time()))
+ )
+ elif self.time_stamp == 1:
+ output = "%s %s" % (
+ time.strftime("%b %d %H:%M:%S",
+ time.localtime(time.time())),
+ pre
+ )
+ else:
+ output = pre
+ if self.flag_show:
+ if flag:
+ output = "%s%s%s" % (output, flag, self.flag_show)
+ else:
+ # this call uses the global default, dont print "None", just show the separator
+ output = "%s %s" % (output, self.flag_show)
+ output = "%s%s%s" % (output, msg, suf)
+ if lf:
+ # strip/add lf if needed
+ last_char = output[-1]
+ if lf == 1 and last_char != LINE_FEED:
+ output = output + LINE_FEED
+ elif lf == -1 and last_char == LINE_FEED:
+ output = output[:-1]
+ try:
+ self._fh.write(output)
+ except Exception:
+ # unicode strikes again ;)
+ s = unicode()
+ for i in xrange(len(output)):
+ if ord(output[i]) < 128:
+ c = output[i]
+ else:
+ c = "?"
+ s += c
+ self._fh.write("%s%s%s" % (pre, s, suf))
+ self._fh.flush()
+
+ def is_active(self, flag):
+ """
+ If given flag(s) should generate output.
+ """
+ # try to abort early to quicken code
+ if not self.active:
+ return 0
+ if not flag or flag in self.active:
+ return 1
+ else:
+ # check for multi flag type:
+ if isinstance(flag, (list, tuple)):
+ for s in flag:
+ if s in self.active:
+ return 1
+ return 0
+
+ def active_set(self, active_flags=None):
+ """
+ Returns 1 if any flags where actually set, otherwise 0.
+ """
+ r = 0
+ ok_flags = []
+ if not active_flags:
+ # no debuging at all
+ self.active = []
+ elif isinstance(active_flags, (tuple, list)):
+ flags = self._as_one_list(active_flags)
+ for t in flags:
+ if t not in self.debug_flags:
+ sys.stderr.write("Invalid debugflag given: %s\n" % t)
+ ok_flags.append(t)
+
+ self.active = ok_flags
+ r = 1
+ else:
+ # assume comma string
+ try:
+ flags = active_flags.split(",")
+ except Exception:
+ self.show("***")
+ self.show("*** Invalid debug param given: %s" % active_flags)
+ self.show("*** please correct your param!")
+ self.show("*** due to this, full debuging is enabled")
+ self.active = self.debug_flags
+ for f in flags:
+ s = f.strip()
+ ok_flags.append(s)
+ self.active = ok_flags
+ self._remove_dupe_flags()
+ return r
+
+ def active_get(self):
+ """
+ Returns currently active flags.
+ """
+ return self.active
+
+ def _as_one_list(self, items):
+ """
+ Init param might contain nested lists, typically from group flags.
+ This code organises lst and remves dupes.
+ """
+ if not isinstance(items, (list, tuple)):
+ return [items]
+ r = []
+ for l in items:
+ if isinstance(l, list):
+ lst2 = self._as_one_list(l)
+ for l2 in lst2:
+ self._append_unique_str(r, l2)
+ elif l == None:
+ continue
+ else:
+ self._append_unique_str(r, l)
+ return r
+
+ def _append_unique_str(self, lst, item):
+ """
+ Filter out any dupes.
+ """
+ if not isinstance(item, str):
+ raise Exception("Invalid item type (should be string)", str(item))
+ if item not in lst:
+ lst.append(item)
+ return lst
+
+ def _validate_flag(self, flags):
+ """
+ Verify that flag is defined.
+ """
+ if flags:
+ for flag in self._as_one_list(flags):
+ if not flag in self.debug_flags:
+ raise Exception("Invalid debugflag given", str(flag))
+
+ def _remove_dupe_flags(self):
+ """
+ If multiple instances of Debug is used in same app,
+ some flags might be created multiple time, filter out dupes.
+ """
+ unique_flags = []
+ for f in self.debug_flags:
+ if f not in unique_flags:
+ unique_flags.append(f)
+ self.debug_flags = unique_flags
+
+ colors = {}
+
+ def Show(self, flag, msg, prefix=""):
+ msg = msg.replace("\r", "\\r").replace("\n", "\\n").replace("><", ">\n <")
+ if not colors_enabled:
+ pass
+ elif prefix in self.colors:
+ msg = self.colors[prefix] + msg + color_none
+ else:
+ msg = color_none + msg
+ if not colors_enabled:
+ prefixcolor = ""
+ elif flag in self.colors:
+ prefixcolor = self.colors[flag]
+ else:
+ prefixcolor = color_none
+ if prefix == "error":
+ e = sys.exc_info()
+ if e[0]:
+ msg = msg + "\n" + "".join(traceback_format_exception(e[0], e[1], e[2])).rstrip()
+ prefix = self.prefix + prefixcolor + (flag + " " * 12)[:12] + " " + (prefix + " " * 6)[:6]
+ self.show(msg, flag, prefix)
+
+ def is_active(self, flag):
+ if not self.active:
+ return 0
+ if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active:
+ return 1
+ return 0
+
+DBG_ALWAYS = "always"
+
+# Debug=NoDebug # Uncomment this to effectively disable all debugging and all debugging overhead.
diff --git a/xmpp/dispatcher.py b/xmpp/dispatcher.py index 58cb5ed..fb6710c 100644 --- a/xmpp/dispatcher.py +++ b/xmpp/dispatcher.py @@ -1,483 +1,483 @@ -## transports.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: dispatcher.py, v1.44 2014/01/15 alkorgun Exp $ - -""" -Main xmpppy mechanism. Provides library with methods to assign different handlers -to different XMPP stanzas. -Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that -Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up. -""" - -import simplexml -import sys -import time - -from plugin import PlugIn -from protocol import * -from select import select -from xml.parsers.expat import ExpatError - -DefaultTimeout = 25 -ID = 0 - -DBG_LINE = "dispatcher" - -class Dispatcher(PlugIn): - """ - Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. - Can be plugged out/in to restart these headers (used for SASL f.e.). - """ - def __init__(self): - PlugIn.__init__(self) - self.handlers = {} - self._expected = {} - self._defaultHandler = None - self._pendingExceptions = [] - self._eventHandler = None - self._cycleHandlers = [] - self._exported_methods = [ - self.Process, - self.RegisterHandler, -# self.RegisterDefaultHandler, - self.RegisterEventHandler, - self.UnregisterCycleHandler, - self.RegisterCycleHandler, - self.RegisterHandlerOnce, - self.UnregisterHandler, - self.RegisterProtocol, - self.WaitForResponse, - self.SendAndWaitForResponse, - self.send, - self.SendAndCallForResponse, - self.disconnect, - self.iter - ] - - def dumpHandlers(self): - """ - Return set of user-registered callbacks in it's internal format. - Used within the library to carry user handlers set over Dispatcher replugins. - """ - return self.handlers - - def restoreHandlers(self, handlers): - """ - Restores user-registered callbacks structure from dump previously obtained via dumpHandlers. - Used within the library to carry user handlers set over Dispatcher replugins. - """ - self.handlers = handlers - - def _init(self): - """ - Registers default namespaces/protocols/handlers. Used internally. - """ - self.RegisterNamespace("unknown") - self.RegisterNamespace(NS_STREAMS) - self.RegisterNamespace(self._owner.defaultNamespace) - self.RegisterProtocol("iq", Iq) - self.RegisterProtocol("presence", Presence) - self.RegisterProtocol("message", Message) -# self.RegisterDefaultHandler(self.returnStanzaHandler) - self.RegisterHandler("error", self.streamErrorHandler, xmlns=NS_STREAMS) - - def plugin(self, owner): - """ - Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally. - """ - self._init() - for method in self._old_owners_methods: - if method.__name__ == "send": - self._owner_send = method; break - self._owner.lastErrNode = None - self._owner.lastErr = None - self._owner.lastErrCode = None - self.StreamInit() - - def plugout(self): - """ - Prepares instance to be destructed. - """ - self.Stream.dispatch = None - self.Stream.DEBUG = None - self.Stream.features = None - self.Stream.destroy() - - def StreamInit(self): - """ - Send an initial stream header. - """ - self.Stream = simplexml.NodeBuilder() - self.Stream._dispatch_depth = 2 - self.Stream.dispatch = self.dispatch - self.Stream.stream_header_received = self._check_stream_start - self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Stream.DEBUG = self._owner.DEBUG - self.Stream.features = None - self._metastream = Node("stream:stream") - self._metastream.setNamespace(self._owner.Namespace) - self._metastream.setAttr("version", "1.0") - self._metastream.setAttr("xmlns:stream", NS_STREAMS) - self._metastream.setAttr("to", self._owner.Server) - self._owner.send("<?xml version=\"1.0\"?>%s>" % str(self._metastream)[:-2]) - - def _check_stream_start(self, ns, tag, attrs): - if ns != NS_STREAMS or tag != "stream": - raise ValueError("Incorrect stream start: (%s,%s). Terminating." % (tag, ns)) - - def Process(self, timeout=8): - """ - Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time. - Returns: - 1) length of processed data if some data were processed; - 2) "0" string if no data were processed but link is alive; - 3) 0 (zero) if underlying connection is closed. - Take note that in case of disconnection detect during Process() call - disconnect handlers are called automatically. - """ - for handler in self._cycleHandlers: - handler(self) - if self._pendingExceptions: - e = self._pendingExceptions.pop() - raise e[0], e[1], e[2] - conn = self._owner.Connection - recv, send = select([conn._sock], [conn._sock] if conn._send_queue else [], [], timeout)[:2] - if send: - while conn._send_queue: - conn.send_now(conn._send_queue.pop(0)) - if recv: - try: - data = conn.receive() - except IOError: - return None - try: - self.Stream.Parse(data) - except ExpatError: - pass - if self._pendingExceptions: - e = self._pendingExceptions.pop() - raise e[0], e[1], e[2] - if data: - return len(data) - return "0" - - def RegisterNamespace(self, xmlns, order="info"): - """ - Creates internal structures for newly registered namespace. - You can register handlers for this namespace afterwards. By default one namespace - already registered (jabber:client or jabber:component:accept depending on context. - """ - self.DEBUG("Registering namespace \"%s\"" % xmlns, order) - self.handlers[xmlns] = {} - self.RegisterProtocol("unknown", Protocol, xmlns=xmlns) - self.RegisterProtocol("default", Protocol, xmlns=xmlns) - - def RegisterProtocol(self, tag_name, Proto, xmlns=None, order="info"): - """ - Used to declare some top-level stanza name to dispatcher. - Needed to start registering handlers for such stanzas. - Iq, message and presence protocols are registered by default. - """ - if not xmlns: - xmlns = self._owner.defaultNamespace - self.DEBUG("Registering protocol \"%s\" as %s(%s)" % (tag_name, Proto, xmlns), order) - self.handlers[xmlns][tag_name] = {"type": Proto, "default": []} - - def RegisterNamespaceHandler(self, xmlns, handler, typ="", ns="", makefirst=0, system=0): - """ - Register handler for processing all stanzas for specified namespace. - """ - self.RegisterHandler("default", handler, typ, ns, xmlns, makefirst, system) - - def RegisterHandler(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0): - """Register user callback as stanzas handler of declared type. Callback must take - (if chained, see later) arguments: dispatcher instance (for replying), incomed - return of previous handlers. - The callback must raise xmpp.NodeProcessed just before return if it want preven - callbacks to be called with the same stanza as argument _and_, more importantly - library from returning stanza to sender with error set (to be enabled in 0.2 ve - Arguments: - "name" - name of stanza. F.e. "iq". - "handler" - user callback. - "typ" - value of stanza's "type" attribute. If not specified any value match - "ns" - namespace of child that stanza must contain. - "chained" - chain together output of several handlers. - "makefirst" - insert handler in the beginning of handlers list instead of - adding it to the end. Note that more common handlers (i.e. w/o "typ" and - will be called first nevertheless). - "system" - call handler even if NodeProcessed Exception were raised already. - """ - if not xmlns: - xmlns = self._owner.defaultNamespace - self.DEBUG("Registering handler %s for \"%s\" type->%s ns->%s(%s)" % (handler, name, typ, ns, xmlns), "info") - if not typ and not ns: - typ = "default" - if not self.handlers.has_key(xmlns): - self.RegisterNamespace(xmlns, "warn") - if not self.handlers[xmlns].has_key(name): - self.RegisterProtocol(name, Protocol, xmlns, "warn") - if not self.handlers[xmlns][name].has_key(typ + ns): - self.handlers[xmlns][name][typ + ns] = [] - if makefirst: - self.handlers[xmlns][name][typ + ns].insert(0, {"func": handler, "system": system}) - else: - self.handlers[xmlns][name][typ + ns].append({"func": handler, "system": system}) - - def RegisterHandlerOnce(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0): - """ - Unregister handler after first call (not implemented yet). - """ - if not xmlns: - xmlns = self._owner.defaultNamespace - self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system) - - def UnregisterHandler(self, name, handler, typ="", ns="", xmlns=None): - """ - Unregister handler. "typ" and "ns" must be specified exactly the same as with registering. - """ - if not xmlns: - xmlns = self._owner.defaultNamespace - if not self.handlers.has_key(xmlns): - return None - if not typ and not ns: - typ = "default" - for pack in self.handlers[xmlns][name][typ + ns]: - if handler == pack["func"]: - break - else: - pack = None - try: - self.handlers[xmlns][name][typ + ns].remove(pack) - except ValueError: - pass - - def RegisterDefaultHandler(self, handler): - """ - Specify the handler that will be used if no NodeProcessed exception were raised. - This is returnStanzaHandler by default. - """ - self._defaultHandler = handler - - def RegisterEventHandler(self, handler): - """ - Register handler that will process events. F.e. "FILERECEIVED" event. - """ - self._eventHandler = handler - - def returnStanzaHandler(self, conn, stanza): - """ - Return stanza back to the sender with <feature-not-implemennted/> error set. - """ - if stanza.getType() in ("get", "set"): - conn.send(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED)) - - def streamErrorHandler(self, conn, error): - name, text = "error", error.getData() - for tag in error.getChildren(): - if tag.getNamespace() == NS_XMPP_STREAMS: - if tag.getName() == "text": - text = tag.getData() - else: - name = tag.getName() - if name in stream_exceptions.keys(): - exc = stream_exceptions[name] - else: - exc = StreamError - raise exc((name, text)) - - def RegisterCycleHandler(self, handler): - """ - Register handler that will be called on every Dispatcher.Process() call. - """ - if handler not in self._cycleHandlers: - self._cycleHandlers.append(handler) - - def UnregisterCycleHandler(self, handler): - """ - Unregister handler that will is called on every Dispatcher.Process() call. - """ - if handler in self._cycleHandlers: - self._cycleHandlers.remove(handler) - - def Event(self, realm, event, data): - """ - Raise some event. Takes three arguments: - 1) "realm" - scope of event. Usually a namespace. - 2) "event" - the event itself. F.e. "SUCESSFULL SEND". - 3) data that comes along with event. Depends on event. - """ - if self._eventHandler: - self._eventHandler(realm, event, data) - - def dispatch(self, stanza, session=None, direct=0): - """ - Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. - Called internally. - """ - if not session: - session = self - session.Stream._mini_dom = None - name = stanza.getName() - if not direct and self._owner._route: - if name == "route": - if stanza.getAttr("error") == None: - if len(stanza.getChildren()) == 1: - stanza = stanza.getChildren()[0] - name = stanza.getName() - else: - for each in stanza.getChildren(): - self.dispatch(each, session, direct=1) - return None - elif name == "presence": - return None - elif name in ("features", "bind"): - pass - else: - raise UnsupportedStanzaType(name) - if name == "features": - session.Stream.features = stanza - xmlns = stanza.getNamespace() - if xmlns not in self.handlers: - self.DEBUG("Unknown namespace: " + xmlns, "warn") - xmlns = "unknown" - if name not in self.handlers[xmlns]: - self.DEBUG("Unknown stanza: " + name, "warn") - name = "unknown" - else: - self.DEBUG("Got %s/%s stanza" % (xmlns, name), "ok") - if isinstance(stanza, Node): - stanza = self.handlers[xmlns][name]["type"](node=stanza) - typ = stanza.getType() - if not typ: - typ = "" - stanza.props = stanza.getProperties() - ID = stanza.getID() - session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s" % (name, typ, stanza.props, ID), "ok") - ls = ["default"] # we will use all handlers: - if typ in self.handlers[xmlns][name]: - ls.append(typ) # from very common... - for prop in stanza.props: - if prop in self.handlers[xmlns][name]: - ls.append(prop) - if typ and (typ + prop) in self.handlers[xmlns][name]: - ls.append(typ + prop) # ...to very particular - chain = self.handlers[xmlns]["default"]["default"] - for key in ls: - if key: - chain = chain + self.handlers[xmlns][name][key] - output = "" - if ID in session._expected: - user = 0 - if isinstance(session._expected[ID], tuple): - cb, args = session._expected.pop(ID) - session.DEBUG("Expected stanza arrived. Callback %s(%s) found!" % (cb, args), "ok") - try: - cb(session, stanza, **args) - except NodeProcessed: - pass - else: - session.DEBUG("Expected stanza arrived!", "ok") - session._expected[ID] = stanza - else: - user = 1 - for handler in chain: - if user or handler["system"]: - try: - handler["func"](session, stanza) - except NodeProcessed: - user = 0 - except Exception: - self._pendingExceptions.insert(0, sys.exc_info()) - if user and self._defaultHandler: - self._defaultHandler(session, stanza) - - def WaitForResponse(self, ID, timeout=DefaultTimeout): - """ - Block and wait until stanza with specific "id" attribute will come. - If no such stanza is arrived within timeout, return None. - If operation failed for some reason then owner's attributes - lastErrNode, lastErr and lastErrCode are set accordingly. - """ - self._expected[ID] = None - abort_time = time.time() + timeout - self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID, timeout), "wait") - while not self._expected[ID]: - if not self.Process(0.04): - self._owner.lastErr = "Disconnect" - return None - if time.time() > abort_time: - self._owner.lastErr = "Timeout" - return None - resp = self._expected.pop(ID) - if resp.getErrorCode(): - self._owner.lastErrNode = resp - self._owner.lastErr = resp.getError() - self._owner.lastErrCode = resp.getErrorCode() - return resp - - def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout): - """ - Put stanza on the wire and wait for recipient's response to it. - """ - return self.WaitForResponse(self.send(stanza), timeout) - - def SendAndCallForResponse(self, stanza, func, args={}): - """ - Put stanza on the wire and call back when recipient replies. - Additional callback arguments can be specified in args. - """ - self._expected[self.send(stanza)] = (func, args) - - def send(self, stanza): - """ - Serialize stanza and put it on the wire. Assign an unique ID to it before send. - Returns assigned ID. - """ - if isinstance(stanza, basestring): - return self._owner_send(stanza) - if not isinstance(stanza, Protocol): - id = None - elif not stanza.getID(): - global ID - ID += 1 - id = repr(ID) - stanza.setID(id) - else: - id = stanza.getID() - if self._owner._registered_name and not stanza.getAttr("from"): - stanza.setAttr("from", self._owner._registered_name) - if self._owner._route and stanza.getName() != "bind": - to = self._owner.Server - if stanza.getTo() and stanza.getTo().getDomain(): - to = stanza.getTo().getDomain() - frm = stanza.getFrom() - if frm.getDomain(): - frm = frm.getDomain() - route = Protocol("route", to=to, frm=frm, payload=[stanza]) - stanza = route - stanza.setNamespace(self._owner.Namespace) - stanza.setParent(self._metastream) - self._owner_send(stanza) - return id - - def disconnect(self): - """ - Send a stream terminator and and handle all incoming stanzas before stream closure. - """ - self._owner_send("</stream:stream>") - while self.Process(1): - pass - - iter = type(send)(Process.func_code, Process.func_globals, name = "iter", argdefs = Process.func_defaults, closure = Process.func_closure) +## transports.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: dispatcher.py, v1.44 2014/01/15 alkorgun Exp $
+
+"""
+Main xmpppy mechanism. Provides library with methods to assign different handlers
+to different XMPP stanzas.
+Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that
+Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up.
+"""
+
+import sys
+import time
+from . import simplexml
+
+from .plugin import PlugIn
+from .protocol import *
+from select import select
+from xml.parsers.expat import ExpatError
+
+DefaultTimeout = 25
+ID = 0
+
+DBG_LINE = "dispatcher"
+
+class Dispatcher(PlugIn):
+ """
+ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers.
+ Can be plugged out/in to restart these headers (used for SASL f.e.).
+ """
+ def __init__(self):
+ PlugIn.__init__(self)
+ self.handlers = {}
+ self._expected = {}
+ self._defaultHandler = None
+ self._pendingExceptions = []
+ self._eventHandler = None
+ self._cycleHandlers = []
+ self._exported_methods = [
+ self.Process,
+ self.RegisterHandler,
+# self.RegisterDefaultHandler,
+ self.RegisterEventHandler,
+ self.UnregisterCycleHandler,
+ self.RegisterCycleHandler,
+ self.RegisterHandlerOnce,
+ self.UnregisterHandler,
+ self.RegisterProtocol,
+ self.WaitForResponse,
+ self.SendAndWaitForResponse,
+ self.send,
+ self.SendAndCallForResponse,
+ self.disconnect,
+ self.iter
+ ]
+
+ def dumpHandlers(self):
+ """
+ Return set of user-registered callbacks in it's internal format.
+ Used within the library to carry user handlers set over Dispatcher replugins.
+ """
+ return self.handlers
+
+ def restoreHandlers(self, handlers):
+ """
+ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers.
+ Used within the library to carry user handlers set over Dispatcher replugins.
+ """
+ self.handlers = handlers
+
+ def _init(self):
+ """
+ Registers default namespaces/protocols/handlers. Used internally.
+ """
+ self.RegisterNamespace("unknown")
+ self.RegisterNamespace(NS_STREAMS)
+ self.RegisterNamespace(self._owner.defaultNamespace)
+ self.RegisterProtocol("iq", Iq)
+ self.RegisterProtocol("presence", Presence)
+ self.RegisterProtocol("message", Message)
+# self.RegisterDefaultHandler(self.returnStanzaHandler)
+ self.RegisterHandler("error", self.streamErrorHandler, xmlns=NS_STREAMS)
+
+ def plugin(self, owner):
+ """
+ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.
+ """
+ self._init()
+ for method in self._old_owners_methods:
+ if method.__name__ == "send":
+ self._owner_send = method; break
+ self._owner.lastErrNode = None
+ self._owner.lastErr = None
+ self._owner.lastErrCode = None
+ self.StreamInit()
+
+ def plugout(self):
+ """
+ Prepares instance to be destructed.
+ """
+ self.Stream.dispatch = None
+ self.Stream.DEBUG = None
+ self.Stream.features = None
+ self.Stream.destroy()
+
+ def StreamInit(self):
+ """
+ Send an initial stream header.
+ """
+ self.Stream = simplexml.NodeBuilder()
+ self.Stream._dispatch_depth = 2
+ self.Stream.dispatch = self.dispatch
+ self.Stream.stream_header_received = self._check_stream_start
+ self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER)
+ self.Stream.DEBUG = self._owner.DEBUG
+ self.Stream.features = None
+ self._metastream = Node("stream:stream")
+ self._metastream.setNamespace(self._owner.Namespace)
+ self._metastream.setAttr("version", "1.0")
+ self._metastream.setAttr("xmlns:stream", NS_STREAMS)
+ self._metastream.setAttr("to", self._owner.Server)
+ self._owner.send("<?xml version=\"1.0\"?>%s>" % str(self._metastream)[:-2])
+
+ def _check_stream_start(self, ns, tag, attrs):
+ if ns != NS_STREAMS or tag != "stream":
+ raise ValueError("Incorrect stream start: (%s,%s). Terminating." % (tag, ns))
+
+ def Process(self, timeout=8):
+ """
+ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time.
+ Returns:
+ 1) length of processed data if some data were processed;
+ 2) "0" string if no data were processed but link is alive;
+ 3) 0 (zero) if underlying connection is closed.
+ Take note that in case of disconnection detect during Process() call
+ disconnect handlers are called automatically.
+ """
+ for handler in self._cycleHandlers:
+ handler(self)
+ if self._pendingExceptions:
+ e = self._pendingExceptions.pop()
+ raise e[0](e[1]).with_traceback(e[2])
+ conn = self._owner.Connection
+ recv, send = select([conn._sock], [conn._sock] if conn._send_queue else [], [], timeout)[:2]
+ if send:
+ while conn._send_queue:
+ conn.send_now(conn._send_queue.pop(0))
+ if recv:
+ try:
+ data = conn.receive()
+ except IOError:
+ return None
+ try:
+ self.Stream.Parse(data)
+ except ExpatError:
+ pass
+ if self._pendingExceptions:
+ e = self._pendingExceptions.pop()
+ raise e[0](e[1]).with_traceback(e[2])
+ if data:
+ return len(data)
+ return "0"
+
+ def RegisterNamespace(self, xmlns, order="info"):
+ """
+ Creates internal structures for newly registered namespace.
+ You can register handlers for this namespace afterwards. By default one namespace
+ already registered (jabber:client or jabber:component:accept depending on context.
+ """
+ self.DEBUG("Registering namespace \"%s\"" % xmlns, order)
+ self.handlers[xmlns] = {}
+ self.RegisterProtocol("unknown", Protocol, xmlns=xmlns)
+ self.RegisterProtocol("default", Protocol, xmlns=xmlns)
+
+ def RegisterProtocol(self, tag_name, Proto, xmlns=None, order="info"):
+ """
+ Used to declare some top-level stanza name to dispatcher.
+ Needed to start registering handlers for such stanzas.
+ Iq, message and presence protocols are registered by default.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.DEBUG("Registering protocol \"%s\" as %s(%s)" % (tag_name, Proto, xmlns), order)
+ self.handlers[xmlns][tag_name] = {"type": Proto, "default": []}
+
+ def RegisterNamespaceHandler(self, xmlns, handler, typ="", ns="", makefirst=0, system=0):
+ """
+ Register handler for processing all stanzas for specified namespace.
+ """
+ self.RegisterHandler("default", handler, typ, ns, xmlns, makefirst, system)
+
+ def RegisterHandler(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0):
+ """Register user callback as stanzas handler of declared type. Callback must take
+ (if chained, see later) arguments: dispatcher instance (for replying), incomed
+ return of previous handlers.
+ The callback must raise xmpp.NodeProcessed just before return if it want preven
+ callbacks to be called with the same stanza as argument _and_, more importantly
+ library from returning stanza to sender with error set (to be enabled in 0.2 ve
+ Arguments:
+ "name" - name of stanza. F.e. "iq".
+ "handler" - user callback.
+ "typ" - value of stanza's "type" attribute. If not specified any value match
+ "ns" - namespace of child that stanza must contain.
+ "chained" - chain together output of several handlers.
+ "makefirst" - insert handler in the beginning of handlers list instead of
+ adding it to the end. Note that more common handlers (i.e. w/o "typ" and
+ will be called first nevertheless).
+ "system" - call handler even if NodeProcessed Exception were raised already.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.DEBUG("Registering handler %s for \"%s\" type->%s ns->%s(%s)" % (handler, name, typ, ns, xmlns), "info")
+ if not typ and not ns:
+ typ = "default"
+ if xmlns not in self.handlers:
+ self.RegisterNamespace(xmlns, "warn")
+ if name not in self.handlers[xmlns]:
+ self.RegisterProtocol(name, Protocol, xmlns, "warn")
+ if typ + ns not in self.handlers[xmlns][name]:
+ self.handlers[xmlns][name][typ + ns] = []
+ if makefirst:
+ self.handlers[xmlns][name][typ + ns].insert(0, {"func": handler, "system": system})
+ else:
+ self.handlers[xmlns][name][typ + ns].append({"func": handler, "system": system})
+
+ def RegisterHandlerOnce(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0):
+ """
+ Unregister handler after first call (not implemented yet).
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system)
+
+ def UnregisterHandler(self, name, handler, typ="", ns="", xmlns=None):
+ """
+ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ if xmlns not in self.handlers:
+ return None
+ if not typ and not ns:
+ typ = "default"
+ for pack in self.handlers[xmlns][name][typ + ns]:
+ if handler == pack["func"]:
+ break
+ else:
+ pack = None
+ try:
+ self.handlers[xmlns][name][typ + ns].remove(pack)
+ except ValueError:
+ pass
+
+ def RegisterDefaultHandler(self, handler):
+ """
+ Specify the handler that will be used if no NodeProcessed exception were raised.
+ This is returnStanzaHandler by default.
+ """
+ self._defaultHandler = handler
+
+ def RegisterEventHandler(self, handler):
+ """
+ Register handler that will process events. F.e. "FILERECEIVED" event.
+ """
+ self._eventHandler = handler
+
+ def returnStanzaHandler(self, conn, stanza):
+ """
+ Return stanza back to the sender with <feature-not-implemennted/> error set.
+ """
+ if stanza.getType() in ("get", "set"):
+ conn.send(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED))
+
+ def streamErrorHandler(self, conn, error):
+ name, text = "error", error.getData()
+ for tag in error.getChildren():
+ if tag.getNamespace() == NS_XMPP_STREAMS:
+ if tag.getName() == "text":
+ text = tag.getData()
+ else:
+ name = tag.getName()
+ if name in stream_exceptions.keys():
+ exc = stream_exceptions[name]
+ else:
+ exc = StreamError
+ raise exc((name, text))
+
+ def RegisterCycleHandler(self, handler):
+ """
+ Register handler that will be called on every Dispatcher.Process() call.
+ """
+ if handler not in self._cycleHandlers:
+ self._cycleHandlers.append(handler)
+
+ def UnregisterCycleHandler(self, handler):
+ """
+ Unregister handler that will is called on every Dispatcher.Process() call.
+ """
+ if handler in self._cycleHandlers:
+ self._cycleHandlers.remove(handler)
+
+ def Event(self, realm, event, data):
+ """
+ Raise some event. Takes three arguments:
+ 1) "realm" - scope of event. Usually a namespace.
+ 2) "event" - the event itself. F.e. "SUCESSFULL SEND".
+ 3) data that comes along with event. Depends on event.
+ """
+ if self._eventHandler:
+ self._eventHandler(realm, event, data)
+
+ def dispatch(self, stanza, session=None, direct=0):
+ """
+ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it.
+ Called internally.
+ """
+ if not session:
+ session = self
+ session.Stream._mini_dom = None
+ name = stanza.getName()
+ if not direct and self._owner._route:
+ if name == "route":
+ if stanza.getAttr("error") == None:
+ if len(stanza.getChildren()) == 1:
+ stanza = stanza.getChildren()[0]
+ name = stanza.getName()
+ else:
+ for each in stanza.getChildren():
+ self.dispatch(each, session, direct=1)
+ return None
+ elif name == "presence":
+ return None
+ elif name in ("features", "bind"):
+ pass
+ else:
+ raise UnsupportedStanzaType(name)
+ if name == "features":
+ session.Stream.features = stanza
+ xmlns = stanza.getNamespace()
+ if xmlns not in self.handlers:
+ self.DEBUG("Unknown namespace: " + xmlns, "warn")
+ xmlns = "unknown"
+ if name not in self.handlers[xmlns]:
+ self.DEBUG("Unknown stanza: " + name, "warn")
+ name = "unknown"
+ else:
+ self.DEBUG("Got %s/%s stanza" % (xmlns, name), "ok")
+ if isinstance(stanza, Node):
+ stanza = self.handlers[xmlns][name]["type"](node=stanza)
+ typ = stanza.getType()
+ if not typ:
+ typ = ""
+ stanza.props = stanza.getProperties()
+ ID = stanza.getID()
+ session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s" % (name, typ, stanza.props, ID), "ok")
+ ls = ["default"] # we will use all handlers:
+ if typ in self.handlers[xmlns][name]:
+ ls.append(typ) # from very common...
+ for prop in stanza.props:
+ if prop in self.handlers[xmlns][name]:
+ ls.append(prop)
+ if typ and (typ + prop) in self.handlers[xmlns][name]:
+ ls.append(typ + prop) # ...to very particular
+ chain = self.handlers[xmlns]["default"]["default"]
+ for key in ls:
+ if key:
+ chain = chain + self.handlers[xmlns][name][key]
+ output = ""
+ if ID in session._expected:
+ user = 0
+ if isinstance(session._expected[ID], tuple):
+ cb, args = session._expected.pop(ID)
+ session.DEBUG("Expected stanza arrived. Callback %s(%s) found!" % (cb, args), "ok")
+ try:
+ cb(session, stanza, **args)
+ except NodeProcessed:
+ pass
+ else:
+ session.DEBUG("Expected stanza arrived!", "ok")
+ session._expected[ID] = stanza
+ else:
+ user = 1
+ for handler in chain:
+ if user or handler["system"]:
+ try:
+ handler["func"](session, stanza)
+ except NodeProcessed:
+ user = 0
+ except Exception:
+ self._pendingExceptions.insert(0, sys.exc_info())
+ if user and self._defaultHandler:
+ self._defaultHandler(session, stanza)
+
+ def WaitForResponse(self, ID, timeout=DefaultTimeout):
+ """
+ Block and wait until stanza with specific "id" attribute will come.
+ If no such stanza is arrived within timeout, return None.
+ If operation failed for some reason then owner's attributes
+ lastErrNode, lastErr and lastErrCode are set accordingly.
+ """
+ self._expected[ID] = None
+ abort_time = time.time() + timeout
+ self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID, timeout), "wait")
+ while not self._expected[ID]:
+ if not self.Process(0.04):
+ self._owner.lastErr = "Disconnect"
+ return None
+ if time.time() > abort_time:
+ self._owner.lastErr = "Timeout"
+ return None
+ resp = self._expected.pop(ID)
+ if resp.getErrorCode():
+ self._owner.lastErrNode = resp
+ self._owner.lastErr = resp.getError()
+ self._owner.lastErrCode = resp.getErrorCode()
+ return resp
+
+ def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout):
+ """
+ Put stanza on the wire and wait for recipient's response to it.
+ """
+ return self.WaitForResponse(self.send(stanza), timeout)
+
+ def SendAndCallForResponse(self, stanza, func, args={}):
+ """
+ Put stanza on the wire and call back when recipient replies.
+ Additional callback arguments can be specified in args.
+ """
+ self._expected[self.send(stanza)] = (func, args)
+
+ def send(self, stanza):
+ """
+ Serialize stanza and put it on the wire. Assign an unique ID to it before send.
+ Returns assigned ID.
+ """
+ if isinstance(stanza, basestring):
+ return self._owner_send(stanza)
+ if not isinstance(stanza, Protocol):
+ id = None
+ elif not stanza.getID():
+ global ID
+ ID += 1
+ id = repr(ID)
+ stanza.setID(id)
+ else:
+ id = stanza.getID()
+ if self._owner._registered_name and not stanza.getAttr("from"):
+ stanza.setAttr("from", self._owner._registered_name)
+ if self._owner._route and stanza.getName() != "bind":
+ to = self._owner.Server
+ if stanza.getTo() and stanza.getTo().getDomain():
+ to = stanza.getTo().getDomain()
+ frm = stanza.getFrom()
+ if frm.getDomain():
+ frm = frm.getDomain()
+ route = Protocol("route", to=to, frm=frm, payload=[stanza])
+ stanza = route
+ stanza.setNamespace(self._owner.Namespace)
+ stanza.setParent(self._metastream)
+ self._owner_send(stanza)
+ return id
+
+ def disconnect(self):
+ """
+ Send a stream terminator and and handle all incoming stanzas before stream closure.
+ """
+ self._owner_send("</stream:stream>")
+ while self.Process(1):
+ pass
+
+ iter = type(send)(Process.__code__, Process.__globals__, name = "iter", argdefs = Process.__defaults__, closure = Process.__closure__)
diff --git a/xmpp/features.py b/xmpp/features.py index 8dbcbc3..34a81b0 100644 --- a/xmpp/features.py +++ b/xmpp/features.py @@ -1,230 +1,230 @@ -## features.py -## -## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: features.py, v1.26 2013/10/21 alkorgun Exp $ - -""" -This module contains variable stuff that is not worth splitting into separate modules. -Here is: - DISCO client and agents-to-DISCO and browse-to-DISCO emulators. - IBR and password manager. - jabber:iq:privacy methods -All these methods takes "disp" first argument that should be already connected -(and in most cases already authorised) dispatcher instance. -""" - -from protocol import * - -REGISTER_DATA_RECEIVED = "REGISTER DATA RECEIVED" - -def _discover(disp, ns, jid, node=None, fb2b=0, fb2a=1): - """ - Try to obtain info from the remote object. - If remote object doesn't support disco fall back to browse (if fb2b is true) - and if it doesnt support browse (or fb2b is not true) fall back to agents protocol - (if gb2a is true). Returns obtained info. Used internally. - """ - iq = Iq(to=jid, typ="get", queryNS=ns) - if node: - iq.setQuerynode(node) - rep = disp.SendAndWaitForResponse(iq) - if fb2b and not isResultNode(rep): - rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_BROWSE)) # Fallback to browse - if fb2a and not isResultNode(rep): - rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_AGENTS)) # Fallback to agents - if isResultNode(rep): - return [n for n in rep.getQueryPayload() if isinstance(n, Node)] - return [] - -def discoverItems(disp, jid, node=None): - """ - Query remote object about any items that it contains. Return items list. - """ - ret = [] - for i in _discover(disp, NS_DISCO_ITEMS, jid, node): - if i.getName() == "agent" and i.getTag("name"): - i.setAttr("name", i.getTagData("name")) - ret.append(i.attrs) - return ret - -def discoverInfo(disp, jid, node=None): - """ - Query remote object about info that it publishes. Returns identities and features lists. - """ - identities, features = [], [] - for i in _discover(disp, NS_DISCO_INFO, jid, node): - if i.getName() == "identity": - identities.append(i.attrs) - elif i.getName() == "feature": - features.append(i.getAttr("var")) - elif i.getName() == "agent": - if i.getTag("name"): - i.setAttr("name", i.getTagData("name")) - if i.getTag("description"): - i.setAttr("name", i.getTagData("description")) - identities.append(i.attrs) - if i.getTag("groupchat"): - features.append(NS_GROUPCHAT) - if i.getTag("register"): - features.append(NS_REGISTER) - if i.getTag("search"): - features.append(NS_SEARCH) - return identities, features - -def getRegInfo(disp, host, info={}, sync=True): - """ - Gets registration form from remote host. - You can pre-fill the info dictionary. - F.e. if you are requesting info on registering user joey than specify - info as {"username": "joey"}. See JEP-0077 for details. - "disp" must be connected dispatcher instance. - """ - iq = Iq("get", NS_REGISTER, to=host) - for i in info.keys(): - iq.setTagData(i, info[i]) - if sync: - resp = disp.SendAndWaitForResponse(iq) - _ReceivedRegInfo(disp.Dispatcher, resp, host) - return resp - else: - disp.SendAndCallForResponse(iq, _ReceivedRegInfo, {"agent": host}) - -def _ReceivedRegInfo(con, resp, agent): - iq = Iq("get", NS_REGISTER, to=agent) - if not isResultNode(resp): - return None - df = resp.getTag("query", namespace=NS_REGISTER).getTag("x", namespace=NS_DATA) - if df: - con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, DataForm(node=df))) - return None - df = DataForm(typ="form") - for i in resp.getQueryPayload(): - if not isinstance(i, Iq): - pass - elif i.getName() == "instructions": - df.addInstructions(i.getData()) - else: - df.setField(i.getName()).setValue(i.getData()) - con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, df)) - -def register(disp, host, info): - """ - Perform registration on remote server with provided info. - disp must be connected dispatcher instance. - Returns true or false depending on registration result. - If registration fails you can get additional info from the dispatcher's owner - attributes lastErrNode, lastErr and lastErrCode. - """ - iq = Iq("set", NS_REGISTER, to=host) - if not isinstance(info, dict): - info = info.asDict() - for i in info.keys(): - iq.setTag("query").setTagData(i, info[i]) - resp = disp.SendAndWaitForResponse(iq) - if isResultNode(resp): - return 1 - -def unregister(disp, host): - """ - Unregisters with host (permanently removes account). - disp must be connected and authorized dispatcher instance. - Returns true on success. - """ - resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host, payload=[Node("remove")])) - if isResultNode(resp): - return 1 - -def changePasswordTo(disp, newpassword, host=None): - """ - Changes password on specified or current (if not specified) server. - disp must be connected and authorized dispatcher instance. - Returns true on success.""" - if not host: - host = disp._owner.Server - resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host, - payload=[ - Node("username", payload=[disp._owner.User]), - Node("password", payload=[newpassword]) - ])) - if isResultNode(resp): - return 1 - -def getPrivacyLists(disp): - """ - Requests privacy lists from connected server. - Returns dictionary of existing lists on success. - """ - dict = {"lists": []} - try: - resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY)) - if not isResultNode(resp): - return None - for list in resp.getQueryPayload(): - if list.getName() == "list": - dict["lists"].append(list.getAttr("name")) - else: - dict[list.getName()] = list.getAttr("name") - except Exception: - pass - else: - return dict - -def getPrivacyList(disp, listname): - """ - Requests specific privacy list listname. Returns list of XML nodes (rules) - taken from the server responce. - """ - try: - resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY, payload=[Node("list", {"name": listname})])) - if isResultNode(resp): - return resp.getQueryPayload()[0] - except Exception: - pass - -def setActivePrivacyList(disp, listname=None, typ="active"): - """ - Switches privacy list "listname" to specified type. - By default the type is "active". Returns true on success. - """ - if listname: - attrs = {"name": listname} - else: - attrs = {} - resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node(typ, attrs)])) - if isResultNode(resp): - return 1 - -def setDefaultPrivacyList(disp, listname=None): - """ - Sets the default privacy list as "listname". Returns true on success. - """ - return setActivePrivacyList(disp, listname, "default") - -def setPrivacyList(disp, list): - """ - Set the ruleset. "list" should be the simpleXML node formatted - according to RFC 3921 (XMPP-IM) (I.e. Node("list", {"name": listname}, payload=[...]) ) - Returns true on success. - """ - resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[list])) - if isResultNode(resp): - return 1 - -def delPrivacyList(disp, listname): - """ - Deletes privacy list "listname". Returns true on success. - """ - resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node("list", {"name": listname})])) - if isResultNode(resp): - return 1 +## features.py
+##
+## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: features.py, v1.26 2013/10/21 alkorgun Exp $
+
+"""
+This module contains variable stuff that is not worth splitting into separate modules.
+Here is:
+ DISCO client and agents-to-DISCO and browse-to-DISCO emulators.
+ IBR and password manager.
+ jabber:iq:privacy methods
+All these methods takes "disp" first argument that should be already connected
+(and in most cases already authorised) dispatcher instance.
+"""
+
+from .protocol import *
+
+REGISTER_DATA_RECEIVED = "REGISTER DATA RECEIVED"
+
+def _discover(disp, ns, jid, node=None, fb2b=0, fb2a=1):
+ """
+ Try to obtain info from the remote object.
+ If remote object doesn't support disco fall back to browse (if fb2b is true)
+ and if it doesnt support browse (or fb2b is not true) fall back to agents protocol
+ (if gb2a is true). Returns obtained info. Used internally.
+ """
+ iq = Iq(to=jid, typ="get", queryNS=ns)
+ if node:
+ iq.setQuerynode(node)
+ rep = disp.SendAndWaitForResponse(iq)
+ if fb2b and not isResultNode(rep):
+ rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_BROWSE)) # Fallback to browse
+ if fb2a and not isResultNode(rep):
+ rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_AGENTS)) # Fallback to agents
+ if isResultNode(rep):
+ return [n for n in rep.getQueryPayload() if isinstance(n, Node)]
+ return []
+
+def discoverItems(disp, jid, node=None):
+ """
+ Query remote object about any items that it contains. Return items list.
+ """
+ ret = []
+ for i in _discover(disp, NS_DISCO_ITEMS, jid, node):
+ if i.getName() == "agent" and i.getTag("name"):
+ i.setAttr("name", i.getTagData("name"))
+ ret.append(i.attrs)
+ return ret
+
+def discoverInfo(disp, jid, node=None):
+ """
+ Query remote object about info that it publishes. Returns identities and features lists.
+ """
+ identities, features = [], []
+ for i in _discover(disp, NS_DISCO_INFO, jid, node):
+ if i.getName() == "identity":
+ identities.append(i.attrs)
+ elif i.getName() == "feature":
+ features.append(i.getAttr("var"))
+ elif i.getName() == "agent":
+ if i.getTag("name"):
+ i.setAttr("name", i.getTagData("name"))
+ if i.getTag("description"):
+ i.setAttr("name", i.getTagData("description"))
+ identities.append(i.attrs)
+ if i.getTag("groupchat"):
+ features.append(NS_GROUPCHAT)
+ if i.getTag("register"):
+ features.append(NS_REGISTER)
+ if i.getTag("search"):
+ features.append(NS_SEARCH)
+ return identities, features
+
+def getRegInfo(disp, host, info={}, sync=True):
+ """
+ Gets registration form from remote host.
+ You can pre-fill the info dictionary.
+ F.e. if you are requesting info on registering user joey than specify
+ info as {"username": "joey"}. See JEP-0077 for details.
+ "disp" must be connected dispatcher instance.
+ """
+ iq = Iq("get", NS_REGISTER, to=host)
+ for i in info.keys():
+ iq.setTagData(i, info[i])
+ if sync:
+ resp = disp.SendAndWaitForResponse(iq)
+ _ReceivedRegInfo(disp.Dispatcher, resp, host)
+ return resp
+ else:
+ disp.SendAndCallForResponse(iq, _ReceivedRegInfo, {"agent": host})
+
+def _ReceivedRegInfo(con, resp, agent):
+ iq = Iq("get", NS_REGISTER, to=agent)
+ if not isResultNode(resp):
+ return None
+ df = resp.getTag("query", namespace=NS_REGISTER).getTag("x", namespace=NS_DATA)
+ if df:
+ con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, DataForm(node=df)))
+ return None
+ df = DataForm(typ="form")
+ for i in resp.getQueryPayload():
+ if not isinstance(i, Iq):
+ pass
+ elif i.getName() == "instructions":
+ df.addInstructions(i.getData())
+ else:
+ df.setField(i.getName()).setValue(i.getData())
+ con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, df))
+
+def register(disp, host, info):
+ """
+ Perform registration on remote server with provided info.
+ disp must be connected dispatcher instance.
+ Returns true or false depending on registration result.
+ If registration fails you can get additional info from the dispatcher's owner
+ attributes lastErrNode, lastErr and lastErrCode.
+ """
+ iq = Iq("set", NS_REGISTER, to=host)
+ if not isinstance(info, dict):
+ info = info.asDict()
+ for i in info.keys():
+ iq.setTag("query").setTagData(i, info[i])
+ resp = disp.SendAndWaitForResponse(iq)
+ if isResultNode(resp):
+ return 1
+
+def unregister(disp, host):
+ """
+ Unregisters with host (permanently removes account).
+ disp must be connected and authorized dispatcher instance.
+ Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host, payload=[Node("remove")]))
+ if isResultNode(resp):
+ return 1
+
+def changePasswordTo(disp, newpassword, host=None):
+ """
+ Changes password on specified or current (if not specified) server.
+ disp must be connected and authorized dispatcher instance.
+ Returns true on success."""
+ if not host:
+ host = disp._owner.Server
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host,
+ payload=[
+ Node("username", payload=[disp._owner.User]),
+ Node("password", payload=[newpassword])
+ ]))
+ if isResultNode(resp):
+ return 1
+
+def getPrivacyLists(disp):
+ """
+ Requests privacy lists from connected server.
+ Returns dictionary of existing lists on success.
+ """
+ dict = {"lists": []}
+ try:
+ resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY))
+ if not isResultNode(resp):
+ return None
+ for list in resp.getQueryPayload():
+ if list.getName() == "list":
+ dict["lists"].append(list.getAttr("name"))
+ else:
+ dict[list.getName()] = list.getAttr("name")
+ except Exception:
+ pass
+ else:
+ return dict
+
+def getPrivacyList(disp, listname):
+ """
+ Requests specific privacy list listname. Returns list of XML nodes (rules)
+ taken from the server responce.
+ """
+ try:
+ resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY, payload=[Node("list", {"name": listname})]))
+ if isResultNode(resp):
+ return resp.getQueryPayload()[0]
+ except Exception:
+ pass
+
+def setActivePrivacyList(disp, listname=None, typ="active"):
+ """
+ Switches privacy list "listname" to specified type.
+ By default the type is "active". Returns true on success.
+ """
+ if listname:
+ attrs = {"name": listname}
+ else:
+ attrs = {}
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node(typ, attrs)]))
+ if isResultNode(resp):
+ return 1
+
+def setDefaultPrivacyList(disp, listname=None):
+ """
+ Sets the default privacy list as "listname". Returns true on success.
+ """
+ return setActivePrivacyList(disp, listname, "default")
+
+def setPrivacyList(disp, list):
+ """
+ Set the ruleset. "list" should be the simpleXML node formatted
+ according to RFC 3921 (XMPP-IM) (I.e. Node("list", {"name": listname}, payload=[...]) )
+ Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[list]))
+ if isResultNode(resp):
+ return 1
+
+def delPrivacyList(disp, listname):
+ """
+ Deletes privacy list "listname". Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node("list", {"name": listname})]))
+ if isResultNode(resp):
+ return 1
diff --git a/xmpp/filetransfer.py b/xmpp/filetransfer.py index c6ecdd9..2c7621f 100644 --- a/xmpp/filetransfer.py +++ b/xmpp/filetransfer.py @@ -1,226 +1,226 @@ -## filetransfer.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: filetransfer.py, v1.7 2013/10/21 alkorgun Exp $ - -""" -This module contains IBB class that is the simple implementation of JEP-0047. -Note that this is just a transport for data. You have to negotiate data transfer before -(via StreamInitiation most probably). Unfortunately SI is not implemented yet. -""" - -from base64 import encodestring, decodestring -from dispatcher import PlugIn -from protocol import * - -class IBB(PlugIn): - """ - IBB used to transfer small-sized data chunk over estabilished xmpp connection. - Data is split into small blocks (by default 3000 bytes each), encoded as base 64 - and sent to another entity that compiles these blocks back into the data chunk. - This is very inefficiend but should work under any circumstances. Note that - using IBB normally should be the last resort. - """ - def __init__(self): - """ - Initialise internal variables. - """ - PlugIn.__init__(self) - self.DBG_LINE = "ibb" - self._exported_methods = [self.OpenStream] - self._streams = {} - self._ampnode = Node(NS_AMP + " amp", - payload=[ - Node("rule", {"condition": "deliver-at", "value": "stored", "action": "error"}), - Node("rule", {"condition": "match-resource", "value": "exact", "action": "error"}) - ]) - - def plugin(self, owner): - """ - Register handlers for receiving incoming datastreams. Used internally. - """ - self._owner.RegisterHandlerOnce("iq", self.StreamOpenReplyHandler) - self._owner.RegisterHandler("iq", self.IqHandler, ns=NS_IBB) - self._owner.RegisterHandler("message", self.ReceiveHandler, ns=NS_IBB) - - def IqHandler(self, conn, stanza): - """ - Handles streams state change. Used internally. - """ - typ = stanza.getType() - self.DEBUG("IqHandler called typ->%s" % typ, "info") - if typ == "set" and stanza.getTag("open", namespace=NS_IBB): - self.StreamOpenHandler(conn, stanza) - elif typ == "set" and stanza.getTag("close", namespace=NS_IBB): - self.StreamCloseHandler(conn, stanza) - elif typ == "result": - self.StreamCommitHandler(conn, stanza) - elif typ == "error": - self.StreamOpenReplyHandler(conn, stanza) - else: - conn.send(Error(stanza, ERR_BAD_REQUEST)) - raise NodeProcessed() - - def StreamOpenHandler(self, conn, stanza): - """ - Handles opening of new incoming stream. Used internally. - """ - err = None - sid = stanza.getTagAttr("open", "sid") - blocksize = stanza.getTagAttr("open", "block-size") - self.DEBUG("StreamOpenHandler called sid->%s blocksize->%s" % (sid, blocksize), "info") - try: - blocksize = int(blocksize) - except Exception: - err = ERR_BAD_REQUEST - if not sid or not blocksize: - err = ERR_BAD_REQUEST - elif sid in self._streams.keys(): - err = ERR_UNEXPECTED_REQUEST - if err: - rep = Error(stanza, err) - else: - self.DEBUG("Opening stream: id %s, block-size %s" % (sid, blocksize), "info") - rep = Protocol("iq", stanza.getFrom(), "result", stanza.getTo(), {"id": stanza.getID()}) - self._streams[sid] = { - "direction": "<" + str(stanza.getFrom()), - "block-size": blocksize, - "fp": open("/tmp/xmpp_file_" + sid, "w"), - "seq": 0, - "syn_id": stanza.getID() - } - conn.send(rep) - - def OpenStream(self, sid, to, fp, blocksize=3000): - """ - Start new stream. You should provide stream id "sid", the endpoind jid "to", - the file object containing info for send "fp". Also the desired blocksize can be specified. - Take into account that recommended stanza size is 4k and IBB uses base64 encoding - that increases size of data by 1/3. - """ - if sid in self._streams.keys(): - return None - if not JID(to).getResource(): - return None - self._streams[sid] = {"direction": "|>" + to, "block-size": blocksize, "fp": fp, "seq": 0} - self._owner.RegisterCycleHandler(self.SendHandler) - syn = Protocol("iq", to, "set", payload=[Node(NS_IBB + " open", {"sid": sid, "block-size": blocksize})]) - self._owner.send(syn) - self._streams[sid]["syn_id"] = syn.getID() - return self._streams[sid] - - def SendHandler(self, conn): - """ - Send next portion of data if it is time to do it. Used internally. - """ - self.DEBUG("SendHandler called", "info") - for sid in self._streams.keys(): - stream = self._streams[sid] - if stream["direction"][:2] == "|>": - cont = 1 - elif stream["direction"][0] == ">": - chunk = stream["fp"].read(stream["block-size"]) - if chunk: - datanode = Node(NS_IBB + " data", {"sid": sid, "seq": stream["seq"]}, encodestring(chunk)) - stream["seq"] += 1 - if stream["seq"] == 65536: - stream["seq"] = 0 - conn.send(Protocol("message", stream["direction"][1:], payload=[datanode, self._ampnode])) - else: - conn.send(Protocol("iq", stream["direction"][1:], "set", payload=[Node(NS_IBB + " close", {"sid": sid})])) - conn.Event(self.DBG_LINE, "SUCCESSFULL SEND", stream) - del self._streams[sid] - self._owner.UnregisterCycleHandler(self.SendHandler) - - def ReceiveHandler(self, conn, stanza): - """ - Receive next portion of incoming datastream and store it write - it to temporary file. Used internally. - """ - sid, seq, data = stanza.getTagAttr("data", "sid"), stanza.getTagAttr("data", "seq"), stanza.getTagData("data") - self.DEBUG("ReceiveHandler called sid->%s seq->%s" % (sid, seq), "info") - try: - seq = int(seq) - data = decodestring(data) - except Exception: - seq = data = "" - err = None - if not sid in self._streams.keys(): - err = ERR_ITEM_NOT_FOUND - else: - stream = self._streams[sid] - if not data: - err = ERR_BAD_REQUEST - elif seq != stream["seq"]: - err = ERR_UNEXPECTED_REQUEST - else: - self.DEBUG("Successfull receive sid->%s %s+%s bytes" % (sid, stream["fp"].tell(), len(data)), "ok") - stream["seq"] += 1 - stream["fp"].write(data) - if err: - self.DEBUG("Error on receive: %s" % err, "error") - conn.send(Error(Iq(to=stanza.getFrom(), frm=stanza.getTo(), payload=[Node(NS_IBB + " close")]), err, reply=0)) - - def StreamCloseHandler(self, conn, stanza): - """ - Handle stream closure due to all data transmitted. - Raise xmpppy event specifying successfull data receive. - """ - sid = stanza.getTagAttr("close", "sid") - self.DEBUG("StreamCloseHandler called sid->%s" % sid, "info") - if sid in self._streams.keys(): - conn.send(stanza.buildReply("result")) - conn.Event(self.DBG_LINE, "SUCCESSFULL RECEIVE", self._streams[sid]) - del self._streams[sid] - else: - conn.send(Error(stanza, ERR_ITEM_NOT_FOUND)) - - def StreamBrokenHandler(self, conn, stanza): - """ - Handle stream closure due to all some error while receiving data. - Raise xmpppy event specifying unsuccessfull data receive. - """ - syn_id = stanza.getID() - self.DEBUG("StreamBrokenHandler called syn_id->%s" % syn_id, "info") - for sid in self._streams.keys(): - stream = self._streams[sid] - if stream["syn_id"] == syn_id: - if stream["direction"][0] == "<": - conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream) - else: - conn.Event(self.DBG_LINE, "ERROR ON SEND", stream) - del self._streams[sid] - - def StreamOpenReplyHandler(self, conn, stanza): - """ - Handle remote side reply about is it agree or not to receive our datastream. - Used internally. Raises xmpppy event specfiying if the data transfer is agreed upon. - """ - syn_id = stanza.getID() - self.DEBUG("StreamOpenReplyHandler called syn_id->%s" % syn_id, "info") - for sid in self._streams.keys(): - stream = self._streams[sid] - if stream["syn_id"] == syn_id: - if stanza.getType() == "error": - if stream["direction"][0] == "<": - conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream) - else: - conn.Event(self.DBG_LINE, "ERROR ON SEND", stream) - del self._streams[sid] - elif stanza.getType() == "result": - if stream["direction"][0] == "|": - stream["direction"] = stream["direction"][1:] - conn.Event(self.DBG_LINE, "STREAM COMMITTED", stream) - else: - conn.send(Error(stanza, ERR_UNEXPECTED_REQUEST)) +## filetransfer.py
+##
+## Copyright (C) 2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: filetransfer.py, v1.7 2013/10/21 alkorgun Exp $
+
+"""
+This module contains IBB class that is the simple implementation of JEP-0047.
+Note that this is just a transport for data. You have to negotiate data transfer before
+(via StreamInitiation most probably). Unfortunately SI is not implemented yet.
+"""
+
+from base64 import encodestring, decodestring
+from .dispatcher import PlugIn
+from .protocol import *
+
+class IBB(PlugIn):
+ """
+ IBB used to transfer small-sized data chunk over estabilished xmpp connection.
+ Data is split into small blocks (by default 3000 bytes each), encoded as base 64
+ and sent to another entity that compiles these blocks back into the data chunk.
+ This is very inefficiend but should work under any circumstances. Note that
+ using IBB normally should be the last resort.
+ """
+ def __init__(self):
+ """
+ Initialise internal variables.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "ibb"
+ self._exported_methods = [self.OpenStream]
+ self._streams = {}
+ self._ampnode = Node(NS_AMP + " amp",
+ payload=[
+ Node("rule", {"condition": "deliver-at", "value": "stored", "action": "error"}),
+ Node("rule", {"condition": "match-resource", "value": "exact", "action": "error"})
+ ])
+
+ def plugin(self, owner):
+ """
+ Register handlers for receiving incoming datastreams. Used internally.
+ """
+ self._owner.RegisterHandlerOnce("iq", self.StreamOpenReplyHandler)
+ self._owner.RegisterHandler("iq", self.IqHandler, ns=NS_IBB)
+ self._owner.RegisterHandler("message", self.ReceiveHandler, ns=NS_IBB)
+
+ def IqHandler(self, conn, stanza):
+ """
+ Handles streams state change. Used internally.
+ """
+ typ = stanza.getType()
+ self.DEBUG("IqHandler called typ->%s" % typ, "info")
+ if typ == "set" and stanza.getTag("open", namespace=NS_IBB):
+ self.StreamOpenHandler(conn, stanza)
+ elif typ == "set" and stanza.getTag("close", namespace=NS_IBB):
+ self.StreamCloseHandler(conn, stanza)
+ elif typ == "result":
+ self.StreamCommitHandler(conn, stanza)
+ elif typ == "error":
+ self.StreamOpenReplyHandler(conn, stanza)
+ else:
+ conn.send(Error(stanza, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+
+ def StreamOpenHandler(self, conn, stanza):
+ """
+ Handles opening of new incoming stream. Used internally.
+ """
+ err = None
+ sid = stanza.getTagAttr("open", "sid")
+ blocksize = stanza.getTagAttr("open", "block-size")
+ self.DEBUG("StreamOpenHandler called sid->%s blocksize->%s" % (sid, blocksize), "info")
+ try:
+ blocksize = int(blocksize)
+ except Exception:
+ err = ERR_BAD_REQUEST
+ if not sid or not blocksize:
+ err = ERR_BAD_REQUEST
+ elif sid in self._streams.keys():
+ err = ERR_UNEXPECTED_REQUEST
+ if err:
+ rep = Error(stanza, err)
+ else:
+ self.DEBUG("Opening stream: id %s, block-size %s" % (sid, blocksize), "info")
+ rep = Protocol("iq", stanza.getFrom(), "result", stanza.getTo(), {"id": stanza.getID()})
+ self._streams[sid] = {
+ "direction": "<" + str(stanza.getFrom()),
+ "block-size": blocksize,
+ "fp": open("/tmp/xmpp_file_" + sid, "w"),
+ "seq": 0,
+ "syn_id": stanza.getID()
+ }
+ conn.send(rep)
+
+ def OpenStream(self, sid, to, fp, blocksize=3000):
+ """
+ Start new stream. You should provide stream id "sid", the endpoind jid "to",
+ the file object containing info for send "fp". Also the desired blocksize can be specified.
+ Take into account that recommended stanza size is 4k and IBB uses base64 encoding
+ that increases size of data by 1/3.
+ """
+ if sid in self._streams.keys():
+ return None
+ if not JID(to).getResource():
+ return None
+ self._streams[sid] = {"direction": "|>" + to, "block-size": blocksize, "fp": fp, "seq": 0}
+ self._owner.RegisterCycleHandler(self.SendHandler)
+ syn = Protocol("iq", to, "set", payload=[Node(NS_IBB + " open", {"sid": sid, "block-size": blocksize})])
+ self._owner.send(syn)
+ self._streams[sid]["syn_id"] = syn.getID()
+ return self._streams[sid]
+
+ def SendHandler(self, conn):
+ """
+ Send next portion of data if it is time to do it. Used internally.
+ """
+ self.DEBUG("SendHandler called", "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["direction"][:2] == "|>":
+ cont = 1
+ elif stream["direction"][0] == ">":
+ chunk = stream["fp"].read(stream["block-size"])
+ if chunk:
+ datanode = Node(NS_IBB + " data", {"sid": sid, "seq": stream["seq"]}, encodestring(chunk))
+ stream["seq"] += 1
+ if stream["seq"] == 65536:
+ stream["seq"] = 0
+ conn.send(Protocol("message", stream["direction"][1:], payload=[datanode, self._ampnode]))
+ else:
+ conn.send(Protocol("iq", stream["direction"][1:], "set", payload=[Node(NS_IBB + " close", {"sid": sid})]))
+ conn.Event(self.DBG_LINE, "SUCCESSFULL SEND", stream)
+ del self._streams[sid]
+ self._owner.UnregisterCycleHandler(self.SendHandler)
+
+ def ReceiveHandler(self, conn, stanza):
+ """
+ Receive next portion of incoming datastream and store it write
+ it to temporary file. Used internally.
+ """
+ sid, seq, data = stanza.getTagAttr("data", "sid"), stanza.getTagAttr("data", "seq"), stanza.getTagData("data")
+ self.DEBUG("ReceiveHandler called sid->%s seq->%s" % (sid, seq), "info")
+ try:
+ seq = int(seq)
+ data = decodestring(data)
+ except Exception:
+ seq = data = ""
+ err = None
+ if not sid in self._streams.keys():
+ err = ERR_ITEM_NOT_FOUND
+ else:
+ stream = self._streams[sid]
+ if not data:
+ err = ERR_BAD_REQUEST
+ elif seq != stream["seq"]:
+ err = ERR_UNEXPECTED_REQUEST
+ else:
+ self.DEBUG("Successfull receive sid->%s %s+%s bytes" % (sid, stream["fp"].tell(), len(data)), "ok")
+ stream["seq"] += 1
+ stream["fp"].write(data)
+ if err:
+ self.DEBUG("Error on receive: %s" % err, "error")
+ conn.send(Error(Iq(to=stanza.getFrom(), frm=stanza.getTo(), payload=[Node(NS_IBB + " close")]), err, reply=0))
+
+ def StreamCloseHandler(self, conn, stanza):
+ """
+ Handle stream closure due to all data transmitted.
+ Raise xmpppy event specifying successfull data receive.
+ """
+ sid = stanza.getTagAttr("close", "sid")
+ self.DEBUG("StreamCloseHandler called sid->%s" % sid, "info")
+ if sid in self._streams.keys():
+ conn.send(stanza.buildReply("result"))
+ conn.Event(self.DBG_LINE, "SUCCESSFULL RECEIVE", self._streams[sid])
+ del self._streams[sid]
+ else:
+ conn.send(Error(stanza, ERR_ITEM_NOT_FOUND))
+
+ def StreamBrokenHandler(self, conn, stanza):
+ """
+ Handle stream closure due to all some error while receiving data.
+ Raise xmpppy event specifying unsuccessfull data receive.
+ """
+ syn_id = stanza.getID()
+ self.DEBUG("StreamBrokenHandler called syn_id->%s" % syn_id, "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["syn_id"] == syn_id:
+ if stream["direction"][0] == "<":
+ conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream)
+ else:
+ conn.Event(self.DBG_LINE, "ERROR ON SEND", stream)
+ del self._streams[sid]
+
+ def StreamOpenReplyHandler(self, conn, stanza):
+ """
+ Handle remote side reply about is it agree or not to receive our datastream.
+ Used internally. Raises xmpppy event specfiying if the data transfer is agreed upon.
+ """
+ syn_id = stanza.getID()
+ self.DEBUG("StreamOpenReplyHandler called syn_id->%s" % syn_id, "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["syn_id"] == syn_id:
+ if stanza.getType() == "error":
+ if stream["direction"][0] == "<":
+ conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream)
+ else:
+ conn.Event(self.DBG_LINE, "ERROR ON SEND", stream)
+ del self._streams[sid]
+ elif stanza.getType() == "result":
+ if stream["direction"][0] == "|":
+ stream["direction"] = stream["direction"][1:]
+ conn.Event(self.DBG_LINE, "STREAM COMMITTED", stream)
+ else:
+ conn.send(Error(stanza, ERR_UNEXPECTED_REQUEST))
diff --git a/xmpp/plugin.py b/xmpp/plugin.py index badb9e2..5c25e0a 100644 --- a/xmpp/plugin.py +++ b/xmpp/plugin.py @@ -1,69 +1,70 @@ -## plugin.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: plugin.py, v1.0 2013/10/21 alkorgun Exp $ - -""" -Provides library with all Non-SASL and SASL authentication mechanisms. -Can be used both for client and transport authentication. -""" - -class PlugIn: - """ - Common xmpppy plugins infrastructure: plugging in/out, debugging. - """ - def __init__(self): - self._exported_methods = [] - self.DBG_LINE = self.__class__.__name__.lower() - - def PlugIn(self, owner): - """ - Attach to main instance and register ourself and all our staff in it. - """ - self._owner = owner - if self.DBG_LINE not in owner.debug_flags: - owner.debug_flags.append(self.DBG_LINE) - self.DEBUG("Plugging %s into %s" % (self, self._owner), "start") - if owner.__dict__.has_key(self.__class__.__name__): - return self.DEBUG("Plugging ignored: another instance already plugged.", "error") - self._old_owners_methods = [] - for method in self._exported_methods: - if owner.__dict__.has_key(method.__name__): - self._old_owners_methods.append(owner.__dict__[method.__name__]) - owner.__dict__[method.__name__] = method - owner.__dict__[self.__class__.__name__] = self - if self.__class__.__dict__.has_key("plugin"): - return self.plugin(owner) - - def PlugOut(self): - """ - Unregister all our staff from main instance and detach from it. - """ - self.DEBUG("Plugging %s out of %s." % (self, self._owner), "stop") - ret = None - if self.__class__.__dict__.has_key("plugout"): - ret = self.plugout() - self._owner.debug_flags.remove(self.DBG_LINE) - for method in self._exported_methods: - del self._owner.__dict__[method.__name__] - for method in self._old_owners_methods: - self._owner.__dict__[method.__name__] = method - del self._owner.__dict__[self.__class__.__name__] - return ret - - def DEBUG(self, text, severity="info"): - """ - Feed a provided debug line to main instance's debug facility along with our ID string. - """ - self._owner.DEBUG(self.DBG_LINE, text, severity) +## plugin.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: plugin.py, v1.0 2013/10/21 alkorgun Exp $
+
+"""
+Provides library with all Non-SASL and SASL authentication mechanisms.
+Can be used both for client and transport authentication.
+"""
+
+class PlugIn:
+ """
+ Common xmpppy plugins infrastructure: plugging in/out, debugging.
+ """
+ def __init__(self):
+ self._exported_methods = []
+ self.DBG_LINE = self.__class__.__name__.lower()
+
+ def PlugIn(self, owner):
+ """
+ Attach to main instance and register ourself and all our staff in it.
+ """
+ self._owner = owner
+ if self.DBG_LINE not in owner.debug_flags:
+ owner.debug_flags.append(self.DBG_LINE)
+ self.DEBUG("Plugging %s into %s" % (self, self._owner), "start")
+ if hasattr(owner, self.__class__.__name__):
+ return self.DEBUG("Plugging ignored: another instance already plugged.", "error")
+ self._old_owners_methods = []
+ for method in self._exported_methods:
+ if hasattr(owner, method.__name__):
+ self._old_owners_methods.append(getattr(owner, method.__name__))
+ setattr(owner, method.__name__, method)
+ setattr(owner, self.__class__.__name__, self)
+ if hasattr(self, "plugin"):
+ return self.plugin(owner)
+
+ def PlugOut(self):
+ """
+ Unregister all our staff from main instance and detach from it.
+ """
+ self.DEBUG("Plugging %s out of %s." % (self, self._owner), "stop")
+ if hasattr(self, "plugout"):
+ rn = self.plugout()
+ else:
+ rn = None
+ self._owner.debug_flags.remove(self.DBG_LINE)
+ for method in self._exported_methods:
+ delattr(self._owner, method.__name__)
+ for method in self._old_owners_methods:
+ setattr(self._owner, method.__name__, method)
+ delattr(self._owner, self.__class__.__name__)
+ return rn
+
+ def DEBUG(self, text, severity="info"):
+ """
+ Feed a provided debug line to main instance's debug facility along with our ID string.
+ """
+ self._owner.DEBUG(self.DBG_LINE, text, severity)
diff --git a/xmpp/protocol.py b/xmpp/protocol.py index 36822c6..e557377 100644 --- a/xmpp/protocol.py +++ b/xmpp/protocol.py @@ -1,1425 +1,1425 @@ -## protocol.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: protocol.py, v1.64 2014/01/10 alkorgun Exp $ - -""" -Protocol module contains tools that is needed for processing of -xmpp-related data structures. -""" - -from simplexml import Node, XML_ls, XMLescape, ustr - -import time - -NS_ACTIVITY = "http://jabber.org/protocol/activity" # XEP-0108 -NS_ADDRESS = "http://jabber.org/protocol/address" # XEP-0033 -NS_ADMIN = "http://jabber.org/protocol/admin" # XEP-0133 -NS_ADMIN_ADD_USER = NS_ADMIN + "#add-user" # XEP-0133 -NS_ADMIN_DELETE_USER = NS_ADMIN + "#delete-user" # XEP-0133 -NS_ADMIN_DISABLE_USER = NS_ADMIN + "#disable-user" # XEP-0133 -NS_ADMIN_REENABLE_USER = NS_ADMIN + "#reenable-user" # XEP-0133 -NS_ADMIN_END_USER_SESSION = NS_ADMIN + "#end-user-session" # XEP-0133 -NS_ADMIN_GET_USER_PASSWORD = NS_ADMIN + "#get-user-password" # XEP-0133 -NS_ADMIN_CHANGE_USER_PASSWORD = NS_ADMIN + "#change-user-password" # XEP-0133 -NS_ADMIN_GET_USER_ROSTER = NS_ADMIN + "#get-user-roster" # XEP-0133 -NS_ADMIN_GET_USER_LASTLOGIN = NS_ADMIN + "#get-user-lastlogin" # XEP-0133 -NS_ADMIN_USER_STATS = NS_ADMIN + "#user-stats" # XEP-0133 -NS_ADMIN_EDIT_BLACKLIST = NS_ADMIN + "#edit-blacklist" # XEP-0133 -NS_ADMIN_EDIT_WHITELIST = NS_ADMIN + "#edit-whitelist" # XEP-0133 -NS_ADMIN_REGISTERED_USERS_NUM = NS_ADMIN + "#get-registered-users-num" # XEP-0133 -NS_ADMIN_DISABLED_USERS_NUM = NS_ADMIN + "#get-disabled-users-num" # XEP-0133 -NS_ADMIN_ONLINE_USERS_NUM = NS_ADMIN + "#get-online-users-num" # XEP-0133 -NS_ADMIN_ACTIVE_USERS_NUM = NS_ADMIN + "#get-active-users-num" # XEP-0133 -NS_ADMIN_IDLE_USERS_NUM = NS_ADMIN + "#get-idle-users-num" # XEP-0133 -NS_ADMIN_REGISTERED_USERS_LIST = NS_ADMIN + "#get-registered-users-list" # XEP-0133 -NS_ADMIN_DISABLED_USERS_LIST = NS_ADMIN + "#get-disabled-users-list" # XEP-0133 -NS_ADMIN_ONLINE_USERS_LIST = NS_ADMIN + "#get-online-users-list" # XEP-0133 -NS_ADMIN_ACTIVE_USERS_LIST = NS_ADMIN + "#get-active-users-list" # XEP-0133 -NS_ADMIN_IDLE_USERS_LIST = NS_ADMIN + "#get-idle-users-list" # XEP-0133 -NS_ADMIN_ANNOUNCE = NS_ADMIN + "#announce" # XEP-0133 -NS_ADMIN_SET_MOTD = NS_ADMIN + "#set-motd" # XEP-0133 -NS_ADMIN_EDIT_MOTD = NS_ADMIN + "#edit-motd" # XEP-0133 -NS_ADMIN_DELETE_MOTD = NS_ADMIN + "#delete-motd" # XEP-0133 -NS_ADMIN_SET_WELCOME = NS_ADMIN + "#set-welcome" # XEP-0133 -NS_ADMIN_DELETE_WELCOME = NS_ADMIN + "#delete-welcome" # XEP-0133 -NS_ADMIN_EDIT_ADMIN = NS_ADMIN + "#edit-admin" # XEP-0133 -NS_ADMIN_RESTART = NS_ADMIN + "#restart" # XEP-0133 -NS_ADMIN_SHUTDOWN = NS_ADMIN + "#shutdown" # XEP-0133 -NS_AGENTS = "jabber:iq:agents" # XEP-0094 (historical) -NS_AMP = "http://jabber.org/protocol/amp" # XEP-0079 -NS_AMP_ERRORS = NS_AMP + "#errors" # XEP-0079 -NS_AUTH = "jabber:iq:auth" # XEP-0078 -NS_AVATAR = "jabber:iq:avatar" # XEP-0008 (historical) -NS_BIND = "urn:ietf:params:xml:ns:xmpp-bind" # RFC 3920 -NS_BROWSE = "jabber:iq:browse" # XEP-0011 (historical) -NS_BYTESTREAM = "http://jabber.org/protocol/bytestreams" # XEP-0065 -NS_CAPS = "http://jabber.org/protocol/caps" # XEP-0115 -NS_CAPTCHA = "urn:xmpp:captcha" # XEP-0158 -NS_CHATSTATES = "http://jabber.org/protocol/chatstates" # XEP-0085 -NS_CLIENT = "jabber:client" # RFC 3921 -NS_COMMANDS = "http://jabber.org/protocol/commands" # XEP-0050 -NS_COMPONENT_ACCEPT = "jabber:component:accept" # XEP-0114 -NS_COMPONENT_1 = "http://jabberd.jabberstudio.org/ns/component/1.0" # Jabberd2 -NS_COMPRESS = "http://jabber.org/protocol/compress" # XEP-0138 -NS_DATA = "jabber:x:data" # XEP-0004 -NS_DATA_LAYOUT = "http://jabber.org/protocol/xdata-layout" # XEP-0141 -NS_DATA_VALIDATE = "http://jabber.org/protocol/xdata-validate" # XEP-0122 -NS_DELAY = "jabber:x:delay" # XEP-0091 (deprecated) -NS_DIALBACK = "jabber:server:dialback" # RFC 3921 -NS_DISCO = "http://jabber.org/protocol/disco" # XEP-0030 -NS_DISCO_INFO = NS_DISCO + "#info" # XEP-0030 -NS_DISCO_ITEMS = NS_DISCO + "#items" # XEP-0030 -NS_ENCRYPTED = "jabber:x:encrypted" # XEP-0027 -NS_EVENT = "jabber:x:event" # XEP-0022 (deprecated) -NS_FEATURE = "http://jabber.org/protocol/feature-neg" # XEP-0020 -NS_FILE = "http://jabber.org/protocol/si/profile/file-transfer" # XEP-0096 -NS_GATEWAY = "jabber:iq:gateway" # XEP-0100 -NS_GEOLOC = "http://jabber.org/protocol/geoloc" # XEP-0080 -NS_GROUPCHAT = "gc-1.0" # XEP-0045 -NS_HTTP_BIND = "http://jabber.org/protocol/httpbind" # XEP-0124 -NS_IBB = "http://jabber.org/protocol/ibb" # XEP-0047 -NS_INVISIBLE = "presence-invisible" # Jabberd2 -NS_IQ = "iq" # Jabberd2 -NS_LAST = "jabber:iq:last" # XEP-0012 -NS_MEDIA = "urn:xmpp:media-element" # XEP-0158 -NS_MESSAGE = "message" # Jabberd2 -NS_MOOD = "http://jabber.org/protocol/mood" # XEP-0107 -NS_MUC = "http://jabber.org/protocol/muc" # XEP-0045 -NS_MUC_ADMIN = NS_MUC + "#admin" # XEP-0045 -NS_MUC_OWNER = NS_MUC + "#owner" # XEP-0045 -NS_MUC_UNIQUE = NS_MUC + "#unique" # XEP-0045 -NS_MUC_USER = NS_MUC + "#user" # XEP-0045 -NS_MUC_REGISTER = NS_MUC + "#register" # XEP-0045 -NS_MUC_REQUEST = NS_MUC + "#request" # XEP-0045 -NS_MUC_ROOMCONFIG = NS_MUC + "#roomconfig" # XEP-0045 -NS_MUC_ROOMINFO = NS_MUC + "#roominfo" # XEP-0045 -NS_MUC_ROOMS = NS_MUC + "#rooms" # XEP-0045 -NS_MUC_TRAFIC = NS_MUC + "#traffic" # XEP-0045 -NS_NICK = "http://jabber.org/protocol/nick" # XEP-0172 -NS_OFFLINE = "http://jabber.org/protocol/offline" # XEP-0013 -NS_OOB = "jabber:x:oob" # XEP-0066 -NS_PHYSLOC = "http://jabber.org/protocol/physloc" # XEP-0112 -NS_PRESENCE = "presence" # Jabberd2 -NS_PRIVACY = "jabber:iq:privacy" # RFC 3921 -NS_PRIVATE = "jabber:iq:private" # XEP-0049 -NS_PUBSUB = "http://jabber.org/protocol/pubsub" # XEP-0060 -NS_RC = "http://jabber.org/protocol/rc" # XEP-0146 -NS_REGISTER = "jabber:iq:register" # XEP-0077 -NS_RECEIPTS = "urn:xmpp:receipts" # XEP-0184 -NS_ROSTER = "jabber:iq:roster" # RFC 3921 -NS_ROSTERX = "http://jabber.org/protocol/rosterx" # XEP-0144 -NS_RPC = "jabber:iq:rpc" # XEP-0009 -NS_SASL = "urn:ietf:params:xml:ns:xmpp-sasl" # RFC 3920 -NS_SEARCH = "jabber:iq:search" # XEP-0055 -NS_SERVER = "jabber:server" # RFC 3921 -NS_SESSION = "urn:ietf:params:xml:ns:xmpp-session" # RFC 3921 -NS_SI = "http://jabber.org/protocol/si" # XEP-0096 -NS_SI_PUB = "http://jabber.org/protocol/sipub" # XEP-0137 -NS_SIGNED = "jabber:x:signed" # XEP-0027 -NS_SOFTWAREINFO = "urn:xmpp:dataforms:softwareinfo" # XEP-0155 -NS_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" # RFC 3920 -NS_STATS = "http://jabber.org/protocol/stats" # XEP-0039 -NS_STREAMS = "http://etherx.jabber.org/streams" # RFC 3920 -NS_TIME = "jabber:iq:time" # XEP-0090 (deprecated) -NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls" # RFC 3920 -NS_URN_ATTENTION = "urn:xmpp:attention:0" # XEP-0224 -NS_URN_OOB = "urn:xmpp:bob" # XEP-0158 -NS_URN_TIME = "urn:xmpp:time" # XEP-0202 -NS_VACATION = "http://jabber.org/protocol/vacation" # XEP-0109 -NS_VCARD = "vcard-temp" # XEP-0054 -NS_VCARD_UPDATE = "vcard-temp:x:update" # XEP-0153 -NS_VERSION = "jabber:iq:version" # XEP-0092 -NS_WAITINGLIST = "http://jabber.org/protocol/waitinglist" # XEP-0130 -NS_XHTML_IM = "http://jabber.org/protocol/xhtml-im" # XEP-0071 -NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" # RFC 3920 -NS_PING = "urn:xmpp:ping" # XEP-0199 - -NS_MUC_FILTER = "http://jabber.ru/muc-filter" - -STREAM_NOT_AUTHORIZED = NS_XMPP_STREAMS + " not-authorized" -STREAM_REMOTE_CONNECTION_FAILED = NS_XMPP_STREAMS + " remote-connection-failed" -SASL_MECHANISM_TOO_WEAK = NS_SASL + " mechanism-too-weak" -STREAM_XML_NOT_WELL_FORMED = NS_XMPP_STREAMS + " xml-not-well-formed" -ERR_JID_MALFORMED = NS_STANZAS + " jid-malformed" -STREAM_SEE_OTHER_HOST = NS_XMPP_STREAMS + " see-other-host" -STREAM_BAD_NAMESPACE_PREFIX = NS_XMPP_STREAMS + " bad-namespace-prefix" -ERR_SERVICE_UNAVAILABLE = NS_STANZAS + " service-unavailable" -STREAM_CONNECTION_TIMEOUT = NS_XMPP_STREAMS + " connection-timeout" -STREAM_UNSUPPORTED_VERSION = NS_XMPP_STREAMS + " unsupported-version" -STREAM_IMPROPER_ADDRESSING = NS_XMPP_STREAMS + " improper-addressing" -STREAM_UNDEFINED_CONDITION = NS_XMPP_STREAMS + " undefined-condition" -SASL_NOT_AUTHORIZED = NS_SASL + " not-authorized" -ERR_GONE = NS_STANZAS + " gone" -SASL_TEMPORARY_AUTH_FAILURE = NS_SASL + " temporary-auth-failure" -ERR_REMOTE_SERVER_NOT_FOUND = NS_STANZAS + " remote-server-not-found" -ERR_UNEXPECTED_REQUEST = NS_STANZAS + " unexpected-request" -ERR_RECIPIENT_UNAVAILABLE = NS_STANZAS + " recipient-unavailable" -ERR_CONFLICT = NS_STANZAS + " conflict" -STREAM_SYSTEM_SHUTDOWN = NS_XMPP_STREAMS + " system-shutdown" -STREAM_BAD_FORMAT = NS_XMPP_STREAMS + " bad-format" -ERR_SUBSCRIPTION_REQUIRED = NS_STANZAS + " subscription-required" -STREAM_INTERNAL_SERVER_ERROR = NS_XMPP_STREAMS + " internal-server-error" -ERR_NOT_AUTHORIZED = NS_STANZAS + " not-authorized" -SASL_ABORTED = NS_SASL + " aborted" -ERR_REGISTRATION_REQUIRED = NS_STANZAS + " registration-required" -ERR_INTERNAL_SERVER_ERROR = NS_STANZAS + " internal-server-error" -SASL_INCORRECT_ENCODING = NS_SASL + " incorrect-encoding" -STREAM_HOST_GONE = NS_XMPP_STREAMS + " host-gone" -STREAM_POLICY_VIOLATION = NS_XMPP_STREAMS + " policy-violation" -STREAM_INVALID_XML = NS_XMPP_STREAMS + " invalid-xml" -STREAM_CONFLICT = NS_XMPP_STREAMS + " conflict" -STREAM_RESOURCE_CONSTRAINT = NS_XMPP_STREAMS + " resource-constraint" -STREAM_UNSUPPORTED_ENCODING = NS_XMPP_STREAMS + " unsupported-encoding" -ERR_NOT_ALLOWED = NS_STANZAS + " not-allowed" -ERR_ITEM_NOT_FOUND = NS_STANZAS + " item-not-found" -ERR_NOT_ACCEPTABLE = NS_STANZAS + " not-acceptable" -STREAM_INVALID_FROM = NS_XMPP_STREAMS + " invalid-from" -ERR_FEATURE_NOT_IMPLEMENTED = NS_STANZAS + " feature-not-implemented" -ERR_BAD_REQUEST = NS_STANZAS + " bad-request" -STREAM_INVALID_ID = NS_XMPP_STREAMS + " invalid-id" -STREAM_HOST_UNKNOWN = NS_XMPP_STREAMS + " host-unknown" -ERR_UNDEFINED_CONDITION = NS_STANZAS + " undefined-condition" -SASL_INVALID_MECHANISM = NS_SASL + " invalid-mechanism" -STREAM_RESTRICTED_XML = NS_XMPP_STREAMS + " restricted-xml" -ERR_RESOURCE_CONSTRAINT = NS_STANZAS + " resource-constraint" -ERR_REMOTE_SERVER_TIMEOUT = NS_STANZAS + " remote-server-timeout" -SASL_INVALID_AUTHZID = NS_SASL + " invalid-authzid" -ERR_PAYMENT_REQUIRED = NS_STANZAS + " payment-required" -STREAM_INVALID_NAMESPACE = NS_XMPP_STREAMS + " invalid-namespace" -ERR_REDIRECT = NS_STANZAS + " redirect" -STREAM_UNSUPPORTED_STANZA_TYPE = NS_XMPP_STREAMS + " unsupported-stanza-type" -ERR_FORBIDDEN = NS_STANZAS + " forbidden" - -ERRORS = { - "urn:ietf:params:xml:ns:xmpp-sasl not-authorized": ["", "", "The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data."], - "urn:ietf:params:xml:ns:xmpp-stanzas payment-required": ["402", "auth", "The requesting entity is not authorized to access the requested service because payment is required."], - "urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak": ["", "", "The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data."], - "urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding": ["", "", "The initiating entity has encoded the stream in an encoding that is not supported by the server."], - "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout": ["504", "wait", "A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time."], - "urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed": ["", "", "The server is unable to properly connect to a remote resource that is required for authentication or authorization."], - "urn:ietf:params:xml:ns:xmpp-streams restricted-xml": ["", "", "The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character."], - "urn:ietf:params:xml:ns:xmpp-streams see-other-host": ["", "", "The server will not provide service to the initiating entity but is redirecting traffic to another host."], - "urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed": ["", "", "The initiating entity has sent XML that is not well-formed."], - "urn:ietf:params:xml:ns:xmpp-stanzas subscription-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because a subscription is required."], - "urn:ietf:params:xml:ns:xmpp-streams internal-server-error": ["", "", "The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream."], - "urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism": ["", "", "The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element."], - "urn:ietf:params:xml:ns:xmpp-streams policy-violation": ["", "", "The entity has violated some local service policy."], - "urn:ietf:params:xml:ns:xmpp-stanzas conflict": ["409", "cancel", "Access cannot be granted because an existing resource or session exists with the same name or address."], - "urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type": ["", "", "The initiating entity has sent a first-level child of the stream that is not supported by the server."], - "urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding": ["", "", "The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data."], - "urn:ietf:params:xml:ns:xmpp-stanzas registration-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because registration is required."], - "urn:ietf:params:xml:ns:xmpp-streams invalid-id": ["", "", "The stream ID or dialback ID is invalid or does not match an ID previously provided."], - "urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid": ["", "", "The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data."], - "urn:ietf:params:xml:ns:xmpp-stanzas bad-request": ["400", "modify", "The sender has sent XML that is malformed or that cannot be processed."], - "urn:ietf:params:xml:ns:xmpp-streams not-authorized": ["", "", "The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation."], - "urn:ietf:params:xml:ns:xmpp-stanzas forbidden": ["403", "auth", "The requesting entity does not possess the required permissions to perform the action."], - "urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure": ["", "", "The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element."], - "urn:ietf:params:xml:ns:xmpp-streams invalid-namespace": ["", "", "The streams namespace name is something other than \http://etherx.jabber.org/streams\" or the dialback namespace name is something other than \"jabber:server:dialback\"."], - "urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented": ["501", "cancel", "The feature requested is not implemented by the recipient or server and therefore cannot be processed."], - "urn:ietf:params:xml:ns:xmpp-streams invalid-xml": ["", "", "The entity has sent invalid XML over the stream to a server that performs validation."], - "urn:ietf:params:xml:ns:xmpp-stanzas item-not-found": ["404", "cancel", "The addressed JID or item requested cannot be found."], - "urn:ietf:params:xml:ns:xmpp-streams host-gone": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server."], - "urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable": ["404", "wait", "The intended recipient is temporarily unavailable."], - "urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable": ["406", "cancel", "The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server."], - "urn:ietf:params:xml:ns:xmpp-streams invalid-from": ["cancel", "", "The JID or hostname provided in a \"from\" address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization."], - "urn:ietf:params:xml:ns:xmpp-streams bad-format": ["", "", "The entity has sent XML that cannot be processed."], - "urn:ietf:params:xml:ns:xmpp-streams resource-constraint": ["", "", "The server lacks the system resources necessary to service the stream."], - "urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition": ["500", "", "The condition is undefined."], - "urn:ietf:params:xml:ns:xmpp-stanzas redirect": ["302", "modify", "The recipient or server is redirecting requests for this information to another entity."], - "urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix": ["", "", "The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix."], - "urn:ietf:params:xml:ns:xmpp-streams system-shutdown": ["", "", "The server is being shut down and all active streams are being closed."], - "urn:ietf:params:xml:ns:xmpp-streams conflict": ["", "", "The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream."], - "urn:ietf:params:xml:ns:xmpp-streams connection-timeout": ["", "", "The entity has not generated any traffic over the stream for some period of time."], - "urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed": ["400", "modify", "The value of the \"to\" attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."], - "urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint": ["500", "wait", "The server or recipient lacks the system resources necessary to service the request."], - "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found": ["404", "cancel", "A remote server or service specified as part or all of the JID of the intended recipient does not exist."], - "urn:ietf:params:xml:ns:xmpp-streams unsupported-version": ["", "", "The value of the \"version\" attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server."], - "urn:ietf:params:xml:ns:xmpp-streams host-unknown": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server."], - "urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request": ["400", "wait", "The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order)."], - "urn:ietf:params:xml:ns:xmpp-streams improper-addressing": ["", "", "A stanza sent between two servers lacks a \"to\" or \"from\" attribute (or the attribute has no value)."], - "urn:ietf:params:xml:ns:xmpp-stanzas not-allowed": ["405", "cancel", "The recipient or server does not allow any entity to perform the action."], - "urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error": ["500", "wait", "The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error."], - "urn:ietf:params:xml:ns:xmpp-stanzas gone": ["302", "modify", "The recipient or server can no longer be contacted at this address."], - "urn:ietf:params:xml:ns:xmpp-streams undefined-condition": ["", "", "The error condition is not one of those defined by the other conditions in this list."], - "urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable": ["503", "cancel", "The server or recipient does not currently provide the requested service."], - "urn:ietf:params:xml:ns:xmpp-stanzas not-authorized": ["401", "auth", "The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials."], - "urn:ietf:params:xml:ns:xmpp-sasl aborted": ["", "", "The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element."] -} - -_errorcodes = { - "302": "redirect", - "400": "unexpected-request", - "401": "not-authorized", - "402": "payment-required", - "403": "forbidden", - "404": "remote-server-not-found", - "405": "not-allowed", - "406": "not-acceptable", - "407": "subscription-required", - "409": "conflict", - "500": "undefined-condition", - "501": "feature-not-implemented", - "503": "service-unavailable", - "504": "remote-server-timeout" -} - -def isResultNode(node): - """ - Returns true if the node is a positive reply. - """ - return (node and node.getType() == "result") - -def isGetNode(node): - """ - Returns true if the node is a positive reply. - """ - return (node and node.getType() == "get") - -def isSetNode(node): - """ - Returns true if the node is a positive reply. - """ - return (node and node.getType() == "set") - -def isErrorNode(node): - """ - Returns true if the node is a negative reply. - """ - return (node and node.getType() == "error") - -class NodeProcessed(Exception): - """ - Exception that should be raised by handler when the handling should be stopped. - """ - -class StreamError(Exception): - """ - Base exception class for stream errors. - """ - -class BadFormat(StreamError): pass - -class BadNamespacePrefix(StreamError): pass - -class Conflict(StreamError): pass - -class ConnectionTimeout(StreamError): pass - -class HostGone(StreamError): pass - -class HostUnknown(StreamError): pass - -class ImproperAddressing(StreamError): pass - -class InternalServerError(StreamError): pass - -class InvalidFrom(StreamError): pass - -class InvalidID(StreamError): pass - -class InvalidNamespace(StreamError): pass - -class InvalidXML(StreamError): pass - -class NotAuthorized(StreamError): pass - -class PolicyViolation(StreamError): pass - -class RemoteConnectionFailed(StreamError): pass - -class ResourceConstraint(StreamError): pass - -class RestrictedXML(StreamError): pass - -class SeeOtherHost(StreamError): pass - -class SystemShutdown(StreamError): pass - -class UndefinedCondition(StreamError): pass - -class UnsupportedEncoding(StreamError): pass - -class UnsupportedStanzaType(StreamError): pass - -class UnsupportedVersion(StreamError): pass - -class XMLNotWellFormed(StreamError): pass - -stream_exceptions = { - "bad-format": BadFormat, - "bad-namespace-prefix": BadNamespacePrefix, - "conflict": Conflict, - "connection-timeout": ConnectionTimeout, - "host-gone": HostGone, - "host-unknown": HostUnknown, - "improper-addressing": ImproperAddressing, - "internal-server-error": InternalServerError, - "invalid-from": InvalidFrom, - "invalid-id": InvalidID, - "invalid-namespace": InvalidNamespace, - "invalid-xml": InvalidXML, - "not-authorized": NotAuthorized, - "policy-violation": PolicyViolation, - "remote-connection-failed": RemoteConnectionFailed, - "resource-constraint": ResourceConstraint, - "restricted-xml": RestrictedXML, - "see-other-host": SeeOtherHost, - "system-shutdown": SystemShutdown, - "undefined-condition": UndefinedCondition, - "unsupported-encoding": UnsupportedEncoding, - "unsupported-stanza-type": UnsupportedStanzaType, - "unsupported-version": UnsupportedVersion, - "xml-not-well-formed": XMLNotWellFormed -} - -class JID: - """ - JID object. JID can be built from string, modified, compared, serialized into string. - """ - def __init__(self, jid=None, node="", domain="", resource=""): - """ - Constructor. JID can be specified as string (jid argument) or as separate parts. - Examples: - JID("node@domain/resource") - JID(node="node", domain="domain.org") - """ - if not jid and not domain: - raise ValueError("JID must contain at least domain name") - elif isinstance(jid, self.__class__): - self.node, self.domain, self.resource = jid.node, jid.domain, jid.resource - elif domain: - self.node, self.domain, self.resource = node, domain, resource - else: - if jid.find("@") + 1: - self.node, jid = jid.split("@", 1) - else: - self.node = "" - if jid.find("/") + 1: - self.domain, self.resource = jid.split("/", 1) - else: - self.domain, self.resource = jid, "" - - def getNode(self): - """ - Return the node part of the JID. - """ - return self.node - - def setNode(self, node): - """ - Set the node part of the JID to new value. Specify None to remove the node part. - """ - self.node = node.lower() - - def getDomain(self): - """ - Return the domain part of the JID. - """ - return self.domain - - def setDomain(self, domain): - """ - Set the domain part of the JID to new value. - """ - self.domain = domain.lower() - - def getResource(self): - """ - Return the resource part of the JID. - """ - return self.resource - - def setResource(self, resource): - """ - Set the resource part of the JID to new value. Specify None to remove the resource part. - """ - self.resource = resource - - def getStripped(self): - """ - Return the bare representation of JID. I.e. string value w/o resource. - """ - return self.__str__(0) - - def __eq__(self, other): - """ - Compare the JID to another instance or to string for equality. - """ - try: - other = JID(other) - except ValueError: - return False - return self.resource == other.resource and self.__str__(0) == other.__str__(0) - - def __ne__(self, other): - """ - Compare the JID to another instance or to string for non-equality. - """ - return not self.__eq__(other) - - def bareMatch(self, other): - """ - Compare the node and domain parts of the JID's for equality. - """ - return self.__str__(0) == JID(other).__str__(0) - - def __str__(self, wresource=1): - """ - Serialize JID into string. - """ - jid = "@".join((self.node, self.domain)) if self.node else self.domain - if wresource and self.resource: - jid = "/".join((jid, self.resource)) - return jid - - def __hash__(self): - """ - Produce hash of the JID, Allows to use JID objects as keys of the dictionary. - """ - return hash(self.__str__()) - -class Protocol(Node): - """ - A "stanza" object class. Contains methods that are common for presences, iqs and messages. - """ - def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): - """ - Constructor, name is the name of the stanza i.e. "message" or "presence" or "iq". - to is the value of "to" attribure, "typ" - "type" attribute - frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition - timestamp - the time value that needs to be stamped over stanza - xmlns - namespace of top stanza node - node - parsed or unparsed stana to be taken as prototype. - """ - if not attrs: - attrs = {} - if to: - attrs["to"] = to - if frm: - attrs["from"] = frm - if typ: - attrs["type"] = typ - Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) - if not node and xmlns: - self.setNamespace(xmlns) - if self["to"]: - self.setTo(self["to"]) - if self["from"]: - self.setFrom(self["from"]) - if node and isinstance(node, self.__class__) and self.__class__ == node.__class__ and self.attrs.has_key("id"): - del self.attrs["id"] - self.timestamp = None - for x in self.getTags("x", namespace=NS_DELAY): - try: - if not self.getTimestamp() or x.getAttr("stamp") < self.getTimestamp(): - self.setTimestamp(x.getAttr("stamp")) - except Exception: - pass - if timestamp is not None: - self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp="" - - def getTo(self): - """ - Return value of the "to" attribute. - """ - try: - to = self["to"] - except Exception: - to = None - return to - - def getFrom(self): - """ - Return value of the "from" attribute. - """ - try: - frm = self["from"] - except Exception: - frm = None - return frm - - def getTimestamp(self): - """ - Return the timestamp in the "yyyymmddThhmmss" format. - """ - return self.timestamp - - def getID(self): - """ - Return the value of the "id" attribute. - """ - return self.getAttr("id") - - def setTo(self, val): - """ - Set the value of the "to" attribute. - """ - self.setAttr("to", JID(val)) - - def getType(self): - """ - Return the value of the "type" attribute. - """ - return self.getAttr("type") - - def setFrom(self, val): - """ - Set the value of the "from" attribute. - """ - self.setAttr("from", JID(val)) - - def setType(self, val): - """ - Set the value of the "type" attribute. - """ - self.setAttr("type", val) - - def setID(self, val): - """ - Set the value of the "id" attribute. - """ - self.setAttr("id", val) - - def getError(self): - """ - Return the error-condition (if present) or the textual description of the error (otherwise). - """ - errtag = self.getTag("error") - if errtag: - for tag in errtag.getChildren(): - if tag.getName() != "text": - return tag.getName() - return errtag.getData() - - def getErrorCode(self): - """ - Return the error code. Obsolette. - """ - return self.getTagAttr("error", "code") - - def setError(self, error, code=None): - """ - Set the error code. Obsolette. Use error-conditions instead. - """ - if code: - if str(code) in _errorcodes.keys(): - error = ErrorNode(_errorcodes[str(code)], text=error) - else: - error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code, typ="cancel", text=error) - elif isinstance(error, basestring): - error = ErrorNode(error) - self.setType("error") - self.addChild(node=error) - - def setTimestamp(self, val=None): - """ - Set the timestamp. timestamp should be the yyyymmddThhmmss string. - """ - if not val: - val = time.strftime("%Y%m%dT%H:%M:%S", time.gmtime()) - self.timestamp = val - self.setTag("x", {"stamp": self.timestamp}, namespace=NS_DELAY) - - def getProperties(self): - """ - Return the list of namespaces to which belongs the direct childs of element. - """ - props = [] - for child in self.getChildren(): - prop = child.getNamespace() - if prop not in props: - props.append(prop) - return props - - def __setitem__(self, item, val): - """ - Set the item "item" to the value "val". - """ - if item in ["to", "from"]: - val = JID(val) - return self.setAttr(item, val) - -class Message(Protocol): - """ - XMPP Message stanza - "push" mechanism. - """ - def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): - """ - Create message object. You can specify recipient, text of message, type of message - any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the "node" parameted to replicate it as message. - """ - Protocol.__init__(self, "message", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if body: - self.setBody(body) - if subject: - self.setSubject(subject) - - def getBody(self): - """ - Returns text of the message. - """ - return self.getTagData("body") - - def getSubject(self): - """ - Returns subject of the message. - """ - return self.getTagData("subject") - - def getThread(self): - """ - Returns thread of the message. - """ - return self.getTagData("thread") - - def setBody(self, val): - """ - Sets the text of the message. - """ - self.setTagData("body", val) - - def setSubject(self, val): - """ - Sets the subject of the message. - """ - self.setTagData("subject", val) - - def setThread(self, val): - """ - Sets the thread of the message. - """ - self.setTagData("thread", val) - - def buildReply(self, text=None): - """ - Builds and returns another message object with specified text. - The to, from type and thread properties of new message are pre-set as reply to this message. - """ - msg = Message(to=self.getFrom(), frm=self.getTo(), body=text) - thr = self.getThread() - if thr: - msg.setThread(thr) - return msg - -class Presence(Protocol): - """ - XMPP Presence object. - """ - def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): - """ - Create presence object. You can specify recipient, type of message, priority, show and status values - any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the "node" parameted to replicate it as presence. - """ - Protocol.__init__(self, "presence", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if priority: - self.setPriority(priority) - if show: - self.setShow(show) - if status: - self.setStatus(status) - - def getPriority(self): - """ - Returns the priority of the message. - """ - return self.getTagData("priority") - - def getShow(self): - """ - Returns the show value of the message. - """ - return self.getTagData("show") - - def getStatus(self): - """ - Returns the status string of the message. - """ - return self.getTagData("status") - - def setPriority(self, val): - """ - Sets the priority of the message. - """ - self.setTagData("priority", val) - - def setShow(self, val): - """ - Sets the show value of the message. - """ - self.setTagData("show", val) - - def setStatus(self, val): - """ - Sets the status string of the message. - """ - self.setTagData("status", val) - - def _muc_getItemAttr(self, tag, attr): - for xtag in self.getTags("x", namespace=NS_MUC_USER): - for child in xtag.getTags(tag): - return child.getAttr(attr) - - def _muc_getSubTagDataAttr(self, tag, attr): - for xtag in self.getTags("x", namespace=NS_MUC_USER): - for child in xtag.getTags("item"): - for cchild in child.getTags(tag): - return cchild.getData(), cchild.getAttr(attr) - return None, None - - def getRole(self): - """ - Returns the presence role (for groupchat). - """ - return self._muc_getItemAttr("item", "role") - - def getAffiliation(self): - """Returns the presence affiliation (for groupchat). - """ - return self._muc_getItemAttr("item", "affiliation") - - def getNick(self): - """ - Returns the nick value (for nick change in groupchat). - """ - return self._muc_getItemAttr("item", "nick") - - def getJid(self): - """ - Returns the presence jid (for groupchat). - """ - return self._muc_getItemAttr("item", "jid") - - def getReason(self): - """ - Returns the reason of the presence (for groupchat). - """ - return self._muc_getSubTagDataAttr("reason", "")[0] - - def getActor(self): - """ - Returns the reason of the presence (for groupchat). - """ - return self._muc_getSubTagDataAttr("actor", "jid")[1] - - def getStatusCode(self): - """ - Returns the status code of the presence (for groupchat). - """ - return self._muc_getItemAttr("status", "code") - -class Iq(Protocol): - """ - XMPP Iq object - get/set dialog mechanism. - """ - def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): - """ - Create Iq object. You can specify type, query namespace - any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. - Alternatively you can pass in the other XML object as the "node" parameted to replicate it as an iq. - """ - Protocol.__init__(self, "iq", to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) - if payload: - self.setQueryPayload(payload) - if queryNS: - self.setQueryNS(queryNS) - - def getQuery(self): - """ - Returns the query node. - """ - return self.getTag("query") - - def getQueryNS(self): - """ - Returns the namespace of the "query" child element. - """ - tag = self.getTag("query") - if tag: - return tag.getNamespace() - - def getQuerynode(self): - """ - Returns the "node" attribute value of the "query" child element. - """ - return self.getTagAttr("query", "node") - - def getQueryPayload(self): - """ - Returns the "query" child element payload. - """ - tag = self.getTag("query") - if tag: - return tag.getPayload() - - def getQueryChildren(self): - """ - Returns the "query" child element child nodes. - """ - tag = self.getTag("query") - if tag: - return tag.getChildren() - - def setQuery(self, name=None): - """ - Changes the name of the query node, creates it if needed. - Keep the existing name if none is given (use "query" if it's a creation). - Returns the query node. - """ - query = self.getQuery() - if query is None: - query = self.addChild("query") - if name is not None: - query.setName(name) - return query - - def setQueryNS(self, namespace): - """ - Set the namespace of the "query" child element. - """ - self.setTag("query").setNamespace(namespace) - - def setQueryPayload(self, payload): - """ - Set the "query" child element payload. - """ - self.setTag("query").setPayload(payload) - - def setQuerynode(self, node): - """ - Set the "node" attribute value of the "query" child element. - """ - self.setTagAttr("query", "node", node) - - def buildReply(self, typ): - """ - Builds and returns another Iq object of specified type. - The to, from and query child node of new Iq are pre-set as reply to this Iq. - """ - iq = Iq(typ, to=self.getFrom(), frm=self.getTo(), attrs={"id": self.getID()}) - if self.getTag("query"): - iq.setQueryNS(self.getQueryNS()) - return iq - -class ErrorNode(Node): - """ - XMPP-style error element. - In the case of stanza error should be attached to XMPP stanza. - In the case of stream-level errors should be used separately. - """ - def __init__(self, name, code=None, typ=None, text=None): - """ - Create new error node object. - Mandatory parameter: name - name of error condition. - Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol. - """ - if ERRORS.has_key(name): - cod, type, txt = ERRORS[name] - ns = name.split()[0] - else: - cod, ns, type, txt = "500", NS_STANZAS, "cancel", "" - if typ: - type = typ - if code: - cod = code - if text: - txt = text - Node.__init__(self, "error", {}, [Node(name)]) - if type: - self.setAttr("type", type) - if not cod: - self.setName("stream:error") - if txt: - self.addChild(node=Node(ns + " text", {}, [txt])) - if cod: - self.setAttr("code", cod) - -class Error(Protocol): - """ - Used to quickly transform received stanza into error reply. - """ - def __init__(self, node, error, reply=1): - """ - Create error reply basing on the received "node" stanza and the "error" error condition. - If the "node" is not the received stanza but locally created ("to" and "from" fields needs not swapping) - specify the "reply" argument as false. - """ - if reply: - Protocol.__init__(self, to=node.getFrom(), frm=node.getTo(), node=node) - else: - Protocol.__init__(self, node=node) - self.setError(error) - if node.getType() == "error": - self.__str__ = self.__dupstr__ - - def __dupstr__(self, dup1=None, dup2=None): - """ - Dummy function used as preventor of creating error node in reply to error node. - I.e. you will not be able to serialize "double" error into string. - """ - return "" - -class DataField(Node): - """ - This class is used in the DataForm class to describe the single data item. - If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) - then you will need to work with instances of this class. - """ - def __init__(self, name=None, value=None, typ=None, required=0, label=None, desc=None, options=[], node=None): - """ - Create new data field of specified name,value and type. Also "required", "desc" and "options" fields can be set. - Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new datafiled. - """ - Node.__init__(self, "field", node=node) - if name: - self.setVar(name) - if isinstance(value, (list, tuple)): - self.setValues(value) - elif value: - self.setValue(value) - if typ: - self.setType(typ) - elif not typ and not node: - self.setType("text-single") - if required: - self.setRequired(required) - if label: - self.setLabel(label) - if desc: - self.setDesc(desc) - if options: - self.setOptions(options) - - def setRequired(self, req=1): - """ - Change the state of the "required" flag. - """ - if req: - self.setTag("required") - else: - try: - self.delChild("required") - except ValueError: - return None - - def isRequired(self): - """ - Returns in this field a required one. - """ - return self.getTag("required") - - def setLabel(self, label): - """ - Set the label of this field. - """ - self.setAttr("label", label) - - def getLabel(self): - """ - Return the label of this field. - """ - return self.getAttr("label") - - def setDesc(self, desc): - """ - Set the description of this field. - """ - self.setTagData("desc", desc) - - def getDesc(self): - """ - Return the description of this field. - """ - return self.getTagData("desc") - - def setValue(self, val): - """ - Set the value of this field. - """ - self.setTagData("value", val) - - def getValue(self): - return self.getTagData("value") - - def setValues(self, ls): - """ - Set the values of this field as values-list. - Replaces all previous filed values! If you need to just add a value - use addValue method. - """ - while self.getTag("value"): - self.delChild("value") - for val in ls: - self.addValue(val) - - def addValue(self, val): - """ - Add one more value to this field. Used in "get" iq's or such. - """ - self.addChild("value", {}, [val]) - - def getValues(self): - """ - Return the list of values associated with this field. - """ - ret = [] - for tag in self.getTags("value"): - ret.append(tag.getData()) - return ret - - def getOptions(self): - """ - Return label-option pairs list associated with this field. - """ - ret = [] - for tag in self.getTags("option"): - ret.append([tag.getAttr("label"), tag.getTagData("value")]) - return ret - - def setOptions(self, ls): - """ - Set label-option pairs list associated with this field. - """ - while self.getTag("option"): - self.delChild("option") - for opt in ls: - self.addOption(opt) - - def addOption(self, opt): - """ - Add one more label-option pair to this field. - """ - if isinstance(opt, basestring): - self.addChild("option").setTagData("value", opt) - else: - self.addChild("option", {"label": opt[0]}).setTagData("value", opt[1]) - - def getType(self): - """ - Get type of this field. - """ - return self.getAttr("type") - - def setType(self, val): - """ - Set type of this field. - """ - return self.setAttr("type", val) - - def getVar(self): - """ - Get "var" attribute value of this field. - """ - return self.getAttr("var") - - def setVar(self, val): - """ - Set "var" attribute value of this field. - """ - return self.setAttr("var", val) - -class DataReported(Node): - """ - This class is used in the DataForm class to describe the "reported data field" data items which are used in - "multiple item form results" (as described in XEP-0004). - Represents the fields that will be returned from a search. This information is useful when - you try to use the jabber:iq:search namespace to return dynamic form information. - """ - def __init__(self, node=None): - """ - Create new empty "reported data" field. However, note that, according XEP-0004: - * It MUST contain one or more DataFields. - * Contained DataFields SHOULD possess a "type" and "label" attribute in addition to "var" attribute - * Contained DataFields SHOULD NOT contain a <value/> element. - Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new - dataitem. - """ - Node.__init__(self, "reported", node=node) - if node: - newkids = [] - for n in self.getChildren(): - if n.getName() == "field": - newkids.append(DataField(node=n)) - else: - newkids.append(n) - self.kids = newkids - - def getField(self, name): - """ - Return the datafield object with name "name" (if exists). - """ - return self.getTag("field", attrs={"var": name}) - - def setField(self, name, typ=None, label=None): - """ - Create if nessessary or get the existing datafield object with name "name" and return it. - If created, attributes "type" and "label" are applied to new datafield. - """ - field = self.getField(name) - if not field: - field = self.addChild(node=DataField(name, None, typ, 0, label)) - return field - - def asDict(self): - """ - Represent dataitem as simple dictionary mapping of datafield names to their values. - """ - ret = {} - for field in self.getTags("field"): - name = field.getAttr("var") - typ = field.getType() - if isinstance(typ, basestring) and typ.endswith("-multi"): - val = [] - for i in field.getTags("value"): - val.append(i.getData()) - else: - val = field.getTagData("value") - ret[name] = val - if self.getTag("instructions"): - ret["instructions"] = self.getInstructions() - return ret - - def __getitem__(self, name): - """ - Simple dictionary interface for getting datafields values by their names. - """ - item = self.getField(name) - if item: - return item.getValue() - raise IndexError("No such field") - - def __setitem__(self, name, val): - """ - Simple dictionary interface for setting datafields values by their names. - """ - return self.setField(name).setValue(val) - -class DataItem(Node): - """ - This class is used in the DataForm class to describe data items which are used in "multiple - item form results" (as described in XEP-0004). - """ - def __init__(self, node=None): - """ - Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL - DataFields described in DataReported. - Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new - dataitem. - """ - Node.__init__(self, "item", node=node) - if node: - newkids = [] - for n in self.getChildren(): - if n.getName() == "field": - newkids.append(DataField(node=n)) - else: - newkids.append(n) - self.kids = newkids - - def getField(self, name): - """ - Return the datafield object with name "name" (if exists). - """ - return self.getTag("field", attrs={"var": name}) - - def setField(self, name, value=None, typ=None): - """ - Create if nessessary or get the existing datafield object with name "name" and return it. - """ - field = self.getField(name) - if not field: - field = self.addChild(node=DataField(name, value, typ)) - return field - - def asDict(self): - """ - Represent dataitem as simple dictionary mapping of datafield names to their values. - """ - ret = {} - for field in self.getTags("field"): - name = field.getAttr("var") - typ = field.getType() - if isinstance(typ, basestring) and typ.endswith("-multi"): - val = [] - for i in field.getTags("value"): - val.append(i.getData()) - else: - val = field.getTagData("value") - ret[name] = val - if self.getTag("instructions"): - ret["instructions"] = self.getInstructions() - return ret - - def __getitem__(self, name): - """ - Simple dictionary interface for getting datafields values by their names. - """ - item = self.getField(name) - if item: - return item.getValue() - raise IndexError("No such field") - - def __setitem__(self, name, val): - """ - Simple dictionary interface for setting datafields values by their names. - """ - return self.setField(name).setValue(val) - -class DataForm(Node): - """ - DataForm class. Used for manipulating dataforms in XMPP. - Relevant XEPs: 0004, 0068, 0122. - Can be used in disco, pub-sub and many other applications. - """ - def __init__(self, typ=None, data=[], title=None, node=None): - """ - Create new dataform of type "typ"; "data" is the list of DataReported, - DataItem and DataField instances that this dataform contains; "title" - is the title string. - You can specify the "node" argument as the other node to be used as - base for constructing this dataform. - - Title and instructions is optional and SHOULD NOT contain newlines. - Several instructions MAY be present. - "typ" can be one of ("form" | "submit" | "cancel" | "result" ) - "typ" of reply iq can be ( "result" | "set" | "set" | "result" ) respectively. - "cancel" form can not contain any fields. All other forms contains AT LEAST one field. - "title" MAY be included in forms of type "form" and "result". - """ - Node.__init__(self, "x", node=node) - if node: - newkids = [] - for n in self.getChildren(): - if n.getName() == "field": - newkids.append(DataField(node=n)) - elif n.getName() == "item": - newkids.append(DataItem(node=n)) - elif n.getName() == "reported": - newkids.append(DataReported(node=n)) - else: - newkids.append(n) - self.kids = newkids - if typ: - self.setType(typ) - self.setNamespace(NS_DATA) - if title: - self.setTitle(title) - if isinstance(data, dict): - newdata = [] - for name in data.keys(): - newdata.append(DataField(name, data[name])) - data = newdata - for child in data: - if isinstance(child, basestring): - self.addInstructions(child) - elif isinstance(child, DataField): - self.kids.append(child) - elif isinstance(child, DataItem): - self.kids.append(child) - elif isinstance(child, DataReported): - self.kids.append(child) - else: - self.kids.append(DataField(node=child)) - - def getType(self): - """ - Return the type of dataform. - """ - return self.getAttr("type") - - def setType(self, typ): - """ - Set the type of dataform. - """ - self.setAttr("type", typ) - - def getTitle(self): - """ - Return the title of dataform. - """ - return self.getTagData("title") - - def setTitle(self, text): - """ - Set the title of dataform. - """ - self.setTagData("title", text) - - def getInstructions(self): - """ - Return the instructions of dataform. - """ - return self.getTagData("instructions") - - def setInstructions(self, text): - """ - Set the instructions of dataform. - """ - self.setTagData("instructions", text) - - def addInstructions(self, text): - """ - Add one more instruction to the dataform. - """ - self.addChild("instructions", {}, [text]) - - def getField(self, name): - """ - Return the datafield object with name "name" (if exists). - """ - return self.getTag("field", attrs={"var": name}) - - def setField(self, name, value=None, typ=None): - """ - Create if nessessary or get the existing datafield object with name "name" and return it. - """ - field = self.getField(name) - if not field: - field = self.addChild(node=DataField(name, value, typ)) - return field - - def asDict(self): - """ - Represent dataform as simple dictionary mapping of datafield names to their values. - """ - ret = {} - for field in self.getTags("field"): - name = field.getAttr("var") - typ = field.getType() - if isinstance(typ, basestring) and typ.endswith("-multi"): - val = [] - for i in field.getTags("value"): - val.append(i.getData()) - else: - val = field.getTagData("value") - ret[name] = val - if self.getTag("instructions"): - ret["instructions"] = self.getInstructions() - return ret - - def __getitem__(self, name): - """ - Simple dictionary interface for getting datafields values by their names. - """ - item = self.getField(name) - if item: - return item.getValue() - raise IndexError("No such field") - - def __setitem__(self, name, val): - """ - Simple dictionary interface for setting datafields values by their names. - """ - return self.setField(name).setValue(val) +## protocol.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: protocol.py, v1.64 2014/01/10 alkorgun Exp $
+
+"""
+Protocol module contains tools that is needed for processing of
+xmpp-related data structures.
+"""
+
+import time
+
+from .simplexml import Node, XML_ls, XMLescape, ustr
+
+NS_ACTIVITY = "http://jabber.org/protocol/activity" # XEP-0108
+NS_ADDRESS = "http://jabber.org/protocol/address" # XEP-0033
+NS_ADMIN = "http://jabber.org/protocol/admin" # XEP-0133
+NS_ADMIN_ADD_USER = NS_ADMIN + "#add-user" # XEP-0133
+NS_ADMIN_DELETE_USER = NS_ADMIN + "#delete-user" # XEP-0133
+NS_ADMIN_DISABLE_USER = NS_ADMIN + "#disable-user" # XEP-0133
+NS_ADMIN_REENABLE_USER = NS_ADMIN + "#reenable-user" # XEP-0133
+NS_ADMIN_END_USER_SESSION = NS_ADMIN + "#end-user-session" # XEP-0133
+NS_ADMIN_GET_USER_PASSWORD = NS_ADMIN + "#get-user-password" # XEP-0133
+NS_ADMIN_CHANGE_USER_PASSWORD = NS_ADMIN + "#change-user-password" # XEP-0133
+NS_ADMIN_GET_USER_ROSTER = NS_ADMIN + "#get-user-roster" # XEP-0133
+NS_ADMIN_GET_USER_LASTLOGIN = NS_ADMIN + "#get-user-lastlogin" # XEP-0133
+NS_ADMIN_USER_STATS = NS_ADMIN + "#user-stats" # XEP-0133
+NS_ADMIN_EDIT_BLACKLIST = NS_ADMIN + "#edit-blacklist" # XEP-0133
+NS_ADMIN_EDIT_WHITELIST = NS_ADMIN + "#edit-whitelist" # XEP-0133
+NS_ADMIN_REGISTERED_USERS_NUM = NS_ADMIN + "#get-registered-users-num" # XEP-0133
+NS_ADMIN_DISABLED_USERS_NUM = NS_ADMIN + "#get-disabled-users-num" # XEP-0133
+NS_ADMIN_ONLINE_USERS_NUM = NS_ADMIN + "#get-online-users-num" # XEP-0133
+NS_ADMIN_ACTIVE_USERS_NUM = NS_ADMIN + "#get-active-users-num" # XEP-0133
+NS_ADMIN_IDLE_USERS_NUM = NS_ADMIN + "#get-idle-users-num" # XEP-0133
+NS_ADMIN_REGISTERED_USERS_LIST = NS_ADMIN + "#get-registered-users-list" # XEP-0133
+NS_ADMIN_DISABLED_USERS_LIST = NS_ADMIN + "#get-disabled-users-list" # XEP-0133
+NS_ADMIN_ONLINE_USERS_LIST = NS_ADMIN + "#get-online-users-list" # XEP-0133
+NS_ADMIN_ACTIVE_USERS_LIST = NS_ADMIN + "#get-active-users-list" # XEP-0133
+NS_ADMIN_IDLE_USERS_LIST = NS_ADMIN + "#get-idle-users-list" # XEP-0133
+NS_ADMIN_ANNOUNCE = NS_ADMIN + "#announce" # XEP-0133
+NS_ADMIN_SET_MOTD = NS_ADMIN + "#set-motd" # XEP-0133
+NS_ADMIN_EDIT_MOTD = NS_ADMIN + "#edit-motd" # XEP-0133
+NS_ADMIN_DELETE_MOTD = NS_ADMIN + "#delete-motd" # XEP-0133
+NS_ADMIN_SET_WELCOME = NS_ADMIN + "#set-welcome" # XEP-0133
+NS_ADMIN_DELETE_WELCOME = NS_ADMIN + "#delete-welcome" # XEP-0133
+NS_ADMIN_EDIT_ADMIN = NS_ADMIN + "#edit-admin" # XEP-0133
+NS_ADMIN_RESTART = NS_ADMIN + "#restart" # XEP-0133
+NS_ADMIN_SHUTDOWN = NS_ADMIN + "#shutdown" # XEP-0133
+NS_AGENTS = "jabber:iq:agents" # XEP-0094 (historical)
+NS_AMP = "http://jabber.org/protocol/amp" # XEP-0079
+NS_AMP_ERRORS = NS_AMP + "#errors" # XEP-0079
+NS_AUTH = "jabber:iq:auth" # XEP-0078
+NS_AVATAR = "jabber:iq:avatar" # XEP-0008 (historical)
+NS_BIND = "urn:ietf:params:xml:ns:xmpp-bind" # RFC 3920
+NS_BROWSE = "jabber:iq:browse" # XEP-0011 (historical)
+NS_BYTESTREAM = "http://jabber.org/protocol/bytestreams" # XEP-0065
+NS_CAPS = "http://jabber.org/protocol/caps" # XEP-0115
+NS_CAPTCHA = "urn:xmpp:captcha" # XEP-0158
+NS_CHATSTATES = "http://jabber.org/protocol/chatstates" # XEP-0085
+NS_CLIENT = "jabber:client" # RFC 3921
+NS_COMMANDS = "http://jabber.org/protocol/commands" # XEP-0050
+NS_COMPONENT_ACCEPT = "jabber:component:accept" # XEP-0114
+NS_COMPONENT_1 = "http://jabberd.jabberstudio.org/ns/component/1.0" # Jabberd2
+NS_COMPRESS = "http://jabber.org/protocol/compress" # XEP-0138
+NS_DATA = "jabber:x:data" # XEP-0004
+NS_DATA_LAYOUT = "http://jabber.org/protocol/xdata-layout" # XEP-0141
+NS_DATA_VALIDATE = "http://jabber.org/protocol/xdata-validate" # XEP-0122
+NS_DELAY = "jabber:x:delay" # XEP-0091 (deprecated)
+NS_DIALBACK = "jabber:server:dialback" # RFC 3921
+NS_DISCO = "http://jabber.org/protocol/disco" # XEP-0030
+NS_DISCO_INFO = NS_DISCO + "#info" # XEP-0030
+NS_DISCO_ITEMS = NS_DISCO + "#items" # XEP-0030
+NS_ENCRYPTED = "jabber:x:encrypted" # XEP-0027
+NS_EVENT = "jabber:x:event" # XEP-0022 (deprecated)
+NS_FEATURE = "http://jabber.org/protocol/feature-neg" # XEP-0020
+NS_FILE = "http://jabber.org/protocol/si/profile/file-transfer" # XEP-0096
+NS_GATEWAY = "jabber:iq:gateway" # XEP-0100
+NS_GEOLOC = "http://jabber.org/protocol/geoloc" # XEP-0080
+NS_GROUPCHAT = "gc-1.0" # XEP-0045
+NS_HTTP_BIND = "http://jabber.org/protocol/httpbind" # XEP-0124
+NS_IBB = "http://jabber.org/protocol/ibb" # XEP-0047
+NS_INVISIBLE = "presence-invisible" # Jabberd2
+NS_IQ = "iq" # Jabberd2
+NS_LAST = "jabber:iq:last" # XEP-0012
+NS_MEDIA = "urn:xmpp:media-element" # XEP-0158
+NS_MESSAGE = "message" # Jabberd2
+NS_MOOD = "http://jabber.org/protocol/mood" # XEP-0107
+NS_MUC = "http://jabber.org/protocol/muc" # XEP-0045
+NS_MUC_ADMIN = NS_MUC + "#admin" # XEP-0045
+NS_MUC_OWNER = NS_MUC + "#owner" # XEP-0045
+NS_MUC_UNIQUE = NS_MUC + "#unique" # XEP-0045
+NS_MUC_USER = NS_MUC + "#user" # XEP-0045
+NS_MUC_REGISTER = NS_MUC + "#register" # XEP-0045
+NS_MUC_REQUEST = NS_MUC + "#request" # XEP-0045
+NS_MUC_ROOMCONFIG = NS_MUC + "#roomconfig" # XEP-0045
+NS_MUC_ROOMINFO = NS_MUC + "#roominfo" # XEP-0045
+NS_MUC_ROOMS = NS_MUC + "#rooms" # XEP-0045
+NS_MUC_TRAFIC = NS_MUC + "#traffic" # XEP-0045
+NS_NICK = "http://jabber.org/protocol/nick" # XEP-0172
+NS_OFFLINE = "http://jabber.org/protocol/offline" # XEP-0013
+NS_OOB = "jabber:x:oob" # XEP-0066
+NS_PHYSLOC = "http://jabber.org/protocol/physloc" # XEP-0112
+NS_PRESENCE = "presence" # Jabberd2
+NS_PRIVACY = "jabber:iq:privacy" # RFC 3921
+NS_PRIVATE = "jabber:iq:private" # XEP-0049
+NS_PUBSUB = "http://jabber.org/protocol/pubsub" # XEP-0060
+NS_RC = "http://jabber.org/protocol/rc" # XEP-0146
+NS_REGISTER = "jabber:iq:register" # XEP-0077
+NS_RECEIPTS = "urn:xmpp:receipts" # XEP-0184
+NS_ROSTER = "jabber:iq:roster" # RFC 3921
+NS_ROSTERX = "http://jabber.org/protocol/rosterx" # XEP-0144
+NS_RPC = "jabber:iq:rpc" # XEP-0009
+NS_SASL = "urn:ietf:params:xml:ns:xmpp-sasl" # RFC 3920
+NS_SEARCH = "jabber:iq:search" # XEP-0055
+NS_SERVER = "jabber:server" # RFC 3921
+NS_SESSION = "urn:ietf:params:xml:ns:xmpp-session" # RFC 3921
+NS_SI = "http://jabber.org/protocol/si" # XEP-0096
+NS_SI_PUB = "http://jabber.org/protocol/sipub" # XEP-0137
+NS_SIGNED = "jabber:x:signed" # XEP-0027
+NS_SOFTWAREINFO = "urn:xmpp:dataforms:softwareinfo" # XEP-0155
+NS_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" # RFC 3920
+NS_STATS = "http://jabber.org/protocol/stats" # XEP-0039
+NS_STREAMS = "http://etherx.jabber.org/streams" # RFC 3920
+NS_TIME = "jabber:iq:time" # XEP-0090 (deprecated)
+NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls" # RFC 3920
+NS_URN_ATTENTION = "urn:xmpp:attention:0" # XEP-0224
+NS_URN_OOB = "urn:xmpp:bob" # XEP-0158
+NS_URN_TIME = "urn:xmpp:time" # XEP-0202
+NS_VACATION = "http://jabber.org/protocol/vacation" # XEP-0109
+NS_VCARD = "vcard-temp" # XEP-0054
+NS_VCARD_UPDATE = "vcard-temp:x:update" # XEP-0153
+NS_VERSION = "jabber:iq:version" # XEP-0092
+NS_WAITINGLIST = "http://jabber.org/protocol/waitinglist" # XEP-0130
+NS_XHTML_IM = "http://jabber.org/protocol/xhtml-im" # XEP-0071
+NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" # RFC 3920
+NS_PING = "urn:xmpp:ping" # XEP-0199
+
+NS_MUC_FILTER = "http://jabber.ru/muc-filter"
+
+STREAM_NOT_AUTHORIZED = NS_XMPP_STREAMS + " not-authorized"
+STREAM_REMOTE_CONNECTION_FAILED = NS_XMPP_STREAMS + " remote-connection-failed"
+SASL_MECHANISM_TOO_WEAK = NS_SASL + " mechanism-too-weak"
+STREAM_XML_NOT_WELL_FORMED = NS_XMPP_STREAMS + " xml-not-well-formed"
+ERR_JID_MALFORMED = NS_STANZAS + " jid-malformed"
+STREAM_SEE_OTHER_HOST = NS_XMPP_STREAMS + " see-other-host"
+STREAM_BAD_NAMESPACE_PREFIX = NS_XMPP_STREAMS + " bad-namespace-prefix"
+ERR_SERVICE_UNAVAILABLE = NS_STANZAS + " service-unavailable"
+STREAM_CONNECTION_TIMEOUT = NS_XMPP_STREAMS + " connection-timeout"
+STREAM_UNSUPPORTED_VERSION = NS_XMPP_STREAMS + " unsupported-version"
+STREAM_IMPROPER_ADDRESSING = NS_XMPP_STREAMS + " improper-addressing"
+STREAM_UNDEFINED_CONDITION = NS_XMPP_STREAMS + " undefined-condition"
+SASL_NOT_AUTHORIZED = NS_SASL + " not-authorized"
+ERR_GONE = NS_STANZAS + " gone"
+SASL_TEMPORARY_AUTH_FAILURE = NS_SASL + " temporary-auth-failure"
+ERR_REMOTE_SERVER_NOT_FOUND = NS_STANZAS + " remote-server-not-found"
+ERR_UNEXPECTED_REQUEST = NS_STANZAS + " unexpected-request"
+ERR_RECIPIENT_UNAVAILABLE = NS_STANZAS + " recipient-unavailable"
+ERR_CONFLICT = NS_STANZAS + " conflict"
+STREAM_SYSTEM_SHUTDOWN = NS_XMPP_STREAMS + " system-shutdown"
+STREAM_BAD_FORMAT = NS_XMPP_STREAMS + " bad-format"
+ERR_SUBSCRIPTION_REQUIRED = NS_STANZAS + " subscription-required"
+STREAM_INTERNAL_SERVER_ERROR = NS_XMPP_STREAMS + " internal-server-error"
+ERR_NOT_AUTHORIZED = NS_STANZAS + " not-authorized"
+SASL_ABORTED = NS_SASL + " aborted"
+ERR_REGISTRATION_REQUIRED = NS_STANZAS + " registration-required"
+ERR_INTERNAL_SERVER_ERROR = NS_STANZAS + " internal-server-error"
+SASL_INCORRECT_ENCODING = NS_SASL + " incorrect-encoding"
+STREAM_HOST_GONE = NS_XMPP_STREAMS + " host-gone"
+STREAM_POLICY_VIOLATION = NS_XMPP_STREAMS + " policy-violation"
+STREAM_INVALID_XML = NS_XMPP_STREAMS + " invalid-xml"
+STREAM_CONFLICT = NS_XMPP_STREAMS + " conflict"
+STREAM_RESOURCE_CONSTRAINT = NS_XMPP_STREAMS + " resource-constraint"
+STREAM_UNSUPPORTED_ENCODING = NS_XMPP_STREAMS + " unsupported-encoding"
+ERR_NOT_ALLOWED = NS_STANZAS + " not-allowed"
+ERR_ITEM_NOT_FOUND = NS_STANZAS + " item-not-found"
+ERR_NOT_ACCEPTABLE = NS_STANZAS + " not-acceptable"
+STREAM_INVALID_FROM = NS_XMPP_STREAMS + " invalid-from"
+ERR_FEATURE_NOT_IMPLEMENTED = NS_STANZAS + " feature-not-implemented"
+ERR_BAD_REQUEST = NS_STANZAS + " bad-request"
+STREAM_INVALID_ID = NS_XMPP_STREAMS + " invalid-id"
+STREAM_HOST_UNKNOWN = NS_XMPP_STREAMS + " host-unknown"
+ERR_UNDEFINED_CONDITION = NS_STANZAS + " undefined-condition"
+SASL_INVALID_MECHANISM = NS_SASL + " invalid-mechanism"
+STREAM_RESTRICTED_XML = NS_XMPP_STREAMS + " restricted-xml"
+ERR_RESOURCE_CONSTRAINT = NS_STANZAS + " resource-constraint"
+ERR_REMOTE_SERVER_TIMEOUT = NS_STANZAS + " remote-server-timeout"
+SASL_INVALID_AUTHZID = NS_SASL + " invalid-authzid"
+ERR_PAYMENT_REQUIRED = NS_STANZAS + " payment-required"
+STREAM_INVALID_NAMESPACE = NS_XMPP_STREAMS + " invalid-namespace"
+ERR_REDIRECT = NS_STANZAS + " redirect"
+STREAM_UNSUPPORTED_STANZA_TYPE = NS_XMPP_STREAMS + " unsupported-stanza-type"
+ERR_FORBIDDEN = NS_STANZAS + " forbidden"
+
+ERRORS = {
+ "urn:ietf:params:xml:ns:xmpp-sasl not-authorized": ["", "", "The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas payment-required": ["402", "auth", "The requesting entity is not authorized to access the requested service because payment is required."],
+ "urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak": ["", "", "The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding": ["", "", "The initiating entity has encoded the stream in an encoding that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout": ["504", "wait", "A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time."],
+ "urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed": ["", "", "The server is unable to properly connect to a remote resource that is required for authentication or authorization."],
+ "urn:ietf:params:xml:ns:xmpp-streams restricted-xml": ["", "", "The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character."],
+ "urn:ietf:params:xml:ns:xmpp-streams see-other-host": ["", "", "The server will not provide service to the initiating entity but is redirecting traffic to another host."],
+ "urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed": ["", "", "The initiating entity has sent XML that is not well-formed."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas subscription-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because a subscription is required."],
+ "urn:ietf:params:xml:ns:xmpp-streams internal-server-error": ["", "", "The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream."],
+ "urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism": ["", "", "The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element."],
+ "urn:ietf:params:xml:ns:xmpp-streams policy-violation": ["", "", "The entity has violated some local service policy."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas conflict": ["409", "cancel", "Access cannot be granted because an existing resource or session exists with the same name or address."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type": ["", "", "The initiating entity has sent a first-level child of the stream that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding": ["", "", "The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas registration-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because registration is required."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-id": ["", "", "The stream ID or dialback ID is invalid or does not match an ID previously provided."],
+ "urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid": ["", "", "The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas bad-request": ["400", "modify", "The sender has sent XML that is malformed or that cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams not-authorized": ["", "", "The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas forbidden": ["403", "auth", "The requesting entity does not possess the required permissions to perform the action."],
+ "urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure": ["", "", "The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-namespace": ["", "", "The streams namespace name is something other than \http://etherx.jabber.org/streams\" or the dialback namespace name is something other than \"jabber:server:dialback\"."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented": ["501", "cancel", "The feature requested is not implemented by the recipient or server and therefore cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-xml": ["", "", "The entity has sent invalid XML over the stream to a server that performs validation."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas item-not-found": ["404", "cancel", "The addressed JID or item requested cannot be found."],
+ "urn:ietf:params:xml:ns:xmpp-streams host-gone": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable": ["404", "wait", "The intended recipient is temporarily unavailable."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable": ["406", "cancel", "The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-from": ["cancel", "", "The JID or hostname provided in a \"from\" address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization."],
+ "urn:ietf:params:xml:ns:xmpp-streams bad-format": ["", "", "The entity has sent XML that cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams resource-constraint": ["", "", "The server lacks the system resources necessary to service the stream."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition": ["500", "", "The condition is undefined."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas redirect": ["302", "modify", "The recipient or server is redirecting requests for this information to another entity."],
+ "urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix": ["", "", "The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix."],
+ "urn:ietf:params:xml:ns:xmpp-streams system-shutdown": ["", "", "The server is being shut down and all active streams are being closed."],
+ "urn:ietf:params:xml:ns:xmpp-streams conflict": ["", "", "The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream."],
+ "urn:ietf:params:xml:ns:xmpp-streams connection-timeout": ["", "", "The entity has not generated any traffic over the stream for some period of time."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed": ["400", "modify", "The value of the \"to\" attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint": ["500", "wait", "The server or recipient lacks the system resources necessary to service the request."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found": ["404", "cancel", "A remote server or service specified as part or all of the JID of the intended recipient does not exist."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-version": ["", "", "The value of the \"version\" attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-streams host-unknown": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request": ["400", "wait", "The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order)."],
+ "urn:ietf:params:xml:ns:xmpp-streams improper-addressing": ["", "", "A stanza sent between two servers lacks a \"to\" or \"from\" attribute (or the attribute has no value)."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-allowed": ["405", "cancel", "The recipient or server does not allow any entity to perform the action."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error": ["500", "wait", "The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas gone": ["302", "modify", "The recipient or server can no longer be contacted at this address."],
+ "urn:ietf:params:xml:ns:xmpp-streams undefined-condition": ["", "", "The error condition is not one of those defined by the other conditions in this list."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable": ["503", "cancel", "The server or recipient does not currently provide the requested service."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-authorized": ["401", "auth", "The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials."],
+ "urn:ietf:params:xml:ns:xmpp-sasl aborted": ["", "", "The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element."]
+}
+
+_errorcodes = {
+ "302": "redirect",
+ "400": "unexpected-request",
+ "401": "not-authorized",
+ "402": "payment-required",
+ "403": "forbidden",
+ "404": "remote-server-not-found",
+ "405": "not-allowed",
+ "406": "not-acceptable",
+ "407": "subscription-required",
+ "409": "conflict",
+ "500": "undefined-condition",
+ "501": "feature-not-implemented",
+ "503": "service-unavailable",
+ "504": "remote-server-timeout"
+}
+
+def isResultNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "result")
+
+def isGetNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "get")
+
+def isSetNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "set")
+
+def isErrorNode(node):
+ """
+ Returns true if the node is a negative reply.
+ """
+ return (node and node.getType() == "error")
+
+class NodeProcessed(Exception):
+ """
+ Exception that should be raised by handler when the handling should be stopped.
+ """
+
+class StreamError(Exception):
+ """
+ Base exception class for stream errors.
+ """
+
+class BadFormat(StreamError): pass
+
+class BadNamespacePrefix(StreamError): pass
+
+class Conflict(StreamError): pass
+
+class ConnectionTimeout(StreamError): pass
+
+class HostGone(StreamError): pass
+
+class HostUnknown(StreamError): pass
+
+class ImproperAddressing(StreamError): pass
+
+class InternalServerError(StreamError): pass
+
+class InvalidFrom(StreamError): pass
+
+class InvalidID(StreamError): pass
+
+class InvalidNamespace(StreamError): pass
+
+class InvalidXML(StreamError): pass
+
+class NotAuthorized(StreamError): pass
+
+class PolicyViolation(StreamError): pass
+
+class RemoteConnectionFailed(StreamError): pass
+
+class ResourceConstraint(StreamError): pass
+
+class RestrictedXML(StreamError): pass
+
+class SeeOtherHost(StreamError): pass
+
+class SystemShutdown(StreamError): pass
+
+class UndefinedCondition(StreamError): pass
+
+class UnsupportedEncoding(StreamError): pass
+
+class UnsupportedStanzaType(StreamError): pass
+
+class UnsupportedVersion(StreamError): pass
+
+class XMLNotWellFormed(StreamError): pass
+
+stream_exceptions = {
+ "bad-format": BadFormat,
+ "bad-namespace-prefix": BadNamespacePrefix,
+ "conflict": Conflict,
+ "connection-timeout": ConnectionTimeout,
+ "host-gone": HostGone,
+ "host-unknown": HostUnknown,
+ "improper-addressing": ImproperAddressing,
+ "internal-server-error": InternalServerError,
+ "invalid-from": InvalidFrom,
+ "invalid-id": InvalidID,
+ "invalid-namespace": InvalidNamespace,
+ "invalid-xml": InvalidXML,
+ "not-authorized": NotAuthorized,
+ "policy-violation": PolicyViolation,
+ "remote-connection-failed": RemoteConnectionFailed,
+ "resource-constraint": ResourceConstraint,
+ "restricted-xml": RestrictedXML,
+ "see-other-host": SeeOtherHost,
+ "system-shutdown": SystemShutdown,
+ "undefined-condition": UndefinedCondition,
+ "unsupported-encoding": UnsupportedEncoding,
+ "unsupported-stanza-type": UnsupportedStanzaType,
+ "unsupported-version": UnsupportedVersion,
+ "xml-not-well-formed": XMLNotWellFormed
+}
+
+class JID:
+ """
+ JID object. JID can be built from string, modified, compared, serialized into string.
+ """
+ def __init__(self, jid=None, node="", domain="", resource=""):
+ """
+ Constructor. JID can be specified as string (jid argument) or as separate parts.
+ Examples:
+ JID("node@domain/resource")
+ JID(node="node", domain="domain.org")
+ """
+ if not jid and not domain:
+ raise ValueError("JID must contain at least domain name")
+ elif isinstance(jid, self.__class__):
+ self.node, self.domain, self.resource = jid.node, jid.domain, jid.resource
+ elif domain:
+ self.node, self.domain, self.resource = node, domain, resource
+ else:
+ if jid.find("@") + 1:
+ self.node, jid = jid.split("@", 1)
+ else:
+ self.node = ""
+ if jid.find("/") + 1:
+ self.domain, self.resource = jid.split("/", 1)
+ else:
+ self.domain, self.resource = jid, ""
+
+ def getNode(self):
+ """
+ Return the node part of the JID.
+ """
+ return self.node
+
+ def setNode(self, node):
+ """
+ Set the node part of the JID to new value. Specify None to remove the node part.
+ """
+ self.node = node.lower()
+
+ def getDomain(self):
+ """
+ Return the domain part of the JID.
+ """
+ return self.domain
+
+ def setDomain(self, domain):
+ """
+ Set the domain part of the JID to new value.
+ """
+ self.domain = domain.lower()
+
+ def getResource(self):
+ """
+ Return the resource part of the JID.
+ """
+ return self.resource
+
+ def setResource(self, resource):
+ """
+ Set the resource part of the JID to new value. Specify None to remove the resource part.
+ """
+ self.resource = resource
+
+ def getStripped(self):
+ """
+ Return the bare representation of JID. I.e. string value w/o resource.
+ """
+ return self.__str__(0)
+
+ def __eq__(self, other):
+ """
+ Compare the JID to another instance or to string for equality.
+ """
+ try:
+ other = JID(other)
+ except ValueError:
+ return False
+ return self.resource == other.resource and self.__str__(0) == other.__str__(0)
+
+ def __ne__(self, other):
+ """
+ Compare the JID to another instance or to string for non-equality.
+ """
+ return not self.__eq__(other)
+
+ def bareMatch(self, other):
+ """
+ Compare the node and domain parts of the JID's for equality.
+ """
+ return self.__str__(0) == JID(other).__str__(0)
+
+ def __str__(self, wresource=1):
+ """
+ Serialize JID into string.
+ """
+ jid = "@".join((self.node, self.domain)) if self.node else self.domain
+ if wresource and self.resource:
+ jid = "/".join((jid, self.resource))
+ return jid
+
+ def __hash__(self):
+ """
+ Produce hash of the JID, Allows to use JID objects as keys of the dictionary.
+ """
+ return hash(self.__str__())
+
+class Protocol(Node):
+ """
+ A "stanza" object class. Contains methods that are common for presences, iqs and messages.
+ """
+ def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None):
+ """
+ Constructor, name is the name of the stanza i.e. "message" or "presence" or "iq".
+ to is the value of "to" attribure, "typ" - "type" attribute
+ frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition
+ timestamp - the time value that needs to be stamped over stanza
+ xmlns - namespace of top stanza node
+ node - parsed or unparsed stana to be taken as prototype.
+ """
+ if not attrs:
+ attrs = {}
+ if to:
+ attrs["to"] = to
+ if frm:
+ attrs["from"] = frm
+ if typ:
+ attrs["type"] = typ
+ Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
+ if not node and xmlns:
+ self.setNamespace(xmlns)
+ if self["to"]:
+ self.setTo(self["to"])
+ if self["from"]:
+ self.setFrom(self["from"])
+ if node and isinstance(node, self.__class__) and self.__class__ == node.__class__ and "id" in self.attrs:
+ del self.attrs["id"]
+ self.timestamp = None
+ for x in self.getTags("x", namespace=NS_DELAY):
+ try:
+ if not self.getTimestamp() or x.getAttr("stamp") < self.getTimestamp():
+ self.setTimestamp(x.getAttr("stamp"))
+ except Exception:
+ pass
+ if timestamp is not None:
+ self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp=""
+
+ def getTo(self):
+ """
+ Return value of the "to" attribute.
+ """
+ try:
+ to = self["to"]
+ except Exception:
+ to = None
+ return to
+
+ def getFrom(self):
+ """
+ Return value of the "from" attribute.
+ """
+ try:
+ frm = self["from"]
+ except Exception:
+ frm = None
+ return frm
+
+ def getTimestamp(self):
+ """
+ Return the timestamp in the "yyyymmddThhmmss" format.
+ """
+ return self.timestamp
+
+ def getID(self):
+ """
+ Return the value of the "id" attribute.
+ """
+ return self.getAttr("id")
+
+ def setTo(self, val):
+ """
+ Set the value of the "to" attribute.
+ """
+ self.setAttr("to", JID(val))
+
+ def getType(self):
+ """
+ Return the value of the "type" attribute.
+ """
+ return self.getAttr("type")
+
+ def setFrom(self, val):
+ """
+ Set the value of the "from" attribute.
+ """
+ self.setAttr("from", JID(val))
+
+ def setType(self, val):
+ """
+ Set the value of the "type" attribute.
+ """
+ self.setAttr("type", val)
+
+ def setID(self, val):
+ """
+ Set the value of the "id" attribute.
+ """
+ self.setAttr("id", val)
+
+ def getError(self):
+ """
+ Return the error-condition (if present) or the textual description of the error (otherwise).
+ """
+ errtag = self.getTag("error")
+ if errtag:
+ for tag in errtag.getChildren():
+ if tag.getName() != "text":
+ return tag.getName()
+ return errtag.getData()
+
+ def getErrorCode(self):
+ """
+ Return the error code. Obsolette.
+ """
+ return self.getTagAttr("error", "code")
+
+ def setError(self, error, code=None):
+ """
+ Set the error code. Obsolette. Use error-conditions instead.
+ """
+ if code:
+ if str(code) in _errorcodes.keys():
+ error = ErrorNode(_errorcodes[str(code)], text=error)
+ else:
+ error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code, typ="cancel", text=error)
+ elif isinstance(error, basestring):
+ error = ErrorNode(error)
+ self.setType("error")
+ self.addChild(node=error)
+
+ def setTimestamp(self, val=None):
+ """
+ Set the timestamp. timestamp should be the yyyymmddThhmmss string.
+ """
+ if not val:
+ val = time.strftime("%Y%m%dT%H:%M:%S", time.gmtime())
+ self.timestamp = val
+ self.setTag("x", {"stamp": self.timestamp}, namespace=NS_DELAY)
+
+ def getProperties(self):
+ """
+ Return the list of namespaces to which belongs the direct childs of element.
+ """
+ props = []
+ for child in self.getChildren():
+ prop = child.getNamespace()
+ if prop not in props:
+ props.append(prop)
+ return props
+
+ def __setitem__(self, item, val):
+ """
+ Set the item "item" to the value "val".
+ """
+ if item in ["to", "from"]:
+ val = JID(val)
+ return self.setAttr(item, val)
+
+class Message(Protocol):
+ """
+ XMPP Message stanza - "push" mechanism.
+ """
+ def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
+ """
+ Create message object. You can specify recipient, text of message, type of message
+ any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as message.
+ """
+ Protocol.__init__(self, "message", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+ if body:
+ self.setBody(body)
+ if subject:
+ self.setSubject(subject)
+
+ def getBody(self):
+ """
+ Returns text of the message.
+ """
+ return self.getTagData("body")
+
+ def getSubject(self):
+ """
+ Returns subject of the message.
+ """
+ return self.getTagData("subject")
+
+ def getThread(self):
+ """
+ Returns thread of the message.
+ """
+ return self.getTagData("thread")
+
+ def setBody(self, val):
+ """
+ Sets the text of the message.
+ """
+ self.setTagData("body", val)
+
+ def setSubject(self, val):
+ """
+ Sets the subject of the message.
+ """
+ self.setTagData("subject", val)
+
+ def setThread(self, val):
+ """
+ Sets the thread of the message.
+ """
+ self.setTagData("thread", val)
+
+ def buildReply(self, text=None):
+ """
+ Builds and returns another message object with specified text.
+ The to, from type and thread properties of new message are pre-set as reply to this message.
+ """
+ msg = Message(to=self.getFrom(), frm=self.getTo(), body=text)
+ thr = self.getThread()
+ if thr:
+ msg.setThread(thr)
+ return msg
+
+class Presence(Protocol):
+ """
+ XMPP Presence object.
+ """
+ def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None):
+ """
+ Create presence object. You can specify recipient, type of message, priority, show and status values
+ any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as presence.
+ """
+ Protocol.__init__(self, "presence", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+ if priority:
+ self.setPriority(priority)
+ if show:
+ self.setShow(show)
+ if status:
+ self.setStatus(status)
+
+ def getPriority(self):
+ """
+ Returns the priority of the message.
+ """
+ return self.getTagData("priority")
+
+ def getShow(self):
+ """
+ Returns the show value of the message.
+ """
+ return self.getTagData("show")
+
+ def getStatus(self):
+ """
+ Returns the status string of the message.
+ """
+ return self.getTagData("status")
+
+ def setPriority(self, val):
+ """
+ Sets the priority of the message.
+ """
+ self.setTagData("priority", val)
+
+ def setShow(self, val):
+ """
+ Sets the show value of the message.
+ """
+ self.setTagData("show", val)
+
+ def setStatus(self, val):
+ """
+ Sets the status string of the message.
+ """
+ self.setTagData("status", val)
+
+ def _muc_getItemAttr(self, tag, attr):
+ for xtag in self.getTags("x", namespace=NS_MUC_USER):
+ for child in xtag.getTags(tag):
+ return child.getAttr(attr)
+
+ def _muc_getSubTagDataAttr(self, tag, attr):
+ for xtag in self.getTags("x", namespace=NS_MUC_USER):
+ for child in xtag.getTags("item"):
+ for cchild in child.getTags(tag):
+ return cchild.getData(), cchild.getAttr(attr)
+ return None, None
+
+ def getRole(self):
+ """
+ Returns the presence role (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "role")
+
+ def getAffiliation(self):
+ """Returns the presence affiliation (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "affiliation")
+
+ def getNick(self):
+ """
+ Returns the nick value (for nick change in groupchat).
+ """
+ return self._muc_getItemAttr("item", "nick")
+
+ def getJid(self):
+ """
+ Returns the presence jid (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "jid")
+
+ def getReason(self):
+ """
+ Returns the reason of the presence (for groupchat).
+ """
+ return self._muc_getSubTagDataAttr("reason", "")[0]
+
+ def getActor(self):
+ """
+ Returns the reason of the presence (for groupchat).
+ """
+ return self._muc_getSubTagDataAttr("actor", "jid")[1]
+
+ def getStatusCode(self):
+ """
+ Returns the status code of the presence (for groupchat).
+ """
+ return self._muc_getItemAttr("status", "code")
+
+class Iq(Protocol):
+ """
+ XMPP Iq object - get/set dialog mechanism.
+ """
+ def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None):
+ """
+ Create Iq object. You can specify type, query namespace
+ any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as an iq.
+ """
+ Protocol.__init__(self, "iq", to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node)
+ if payload:
+ self.setQueryPayload(payload)
+ if queryNS:
+ self.setQueryNS(queryNS)
+
+ def getQuery(self):
+ """
+ Returns the query node.
+ """
+ return self.getTag("query")
+
+ def getQueryNS(self):
+ """
+ Returns the namespace of the "query" child element.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getNamespace()
+
+ def getQuerynode(self):
+ """
+ Returns the "node" attribute value of the "query" child element.
+ """
+ return self.getTagAttr("query", "node")
+
+ def getQueryPayload(self):
+ """
+ Returns the "query" child element payload.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getPayload()
+
+ def getQueryChildren(self):
+ """
+ Returns the "query" child element child nodes.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getChildren()
+
+ def setQuery(self, name=None):
+ """
+ Changes the name of the query node, creates it if needed.
+ Keep the existing name if none is given (use "query" if it's a creation).
+ Returns the query node.
+ """
+ query = self.getQuery()
+ if query is None:
+ query = self.addChild("query")
+ if name is not None:
+ query.setName(name)
+ return query
+
+ def setQueryNS(self, namespace):
+ """
+ Set the namespace of the "query" child element.
+ """
+ self.setTag("query").setNamespace(namespace)
+
+ def setQueryPayload(self, payload):
+ """
+ Set the "query" child element payload.
+ """
+ self.setTag("query").setPayload(payload)
+
+ def setQuerynode(self, node):
+ """
+ Set the "node" attribute value of the "query" child element.
+ """
+ self.setTagAttr("query", "node", node)
+
+ def buildReply(self, typ):
+ """
+ Builds and returns another Iq object of specified type.
+ The to, from and query child node of new Iq are pre-set as reply to this Iq.
+ """
+ iq = Iq(typ, to=self.getFrom(), frm=self.getTo(), attrs={"id": self.getID()})
+ if self.getTag("query"):
+ iq.setQueryNS(self.getQueryNS())
+ return iq
+
+class ErrorNode(Node):
+ """
+ XMPP-style error element.
+ In the case of stanza error should be attached to XMPP stanza.
+ In the case of stream-level errors should be used separately.
+ """
+ def __init__(self, name, code=None, typ=None, text=None):
+ """
+ Create new error node object.
+ Mandatory parameter: name - name of error condition.
+ Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.
+ """
+ if name in ERRORS:
+ cod, type, txt = ERRORS[name]
+ ns = name.split()[0]
+ else:
+ cod, ns, type, txt = "500", NS_STANZAS, "cancel", ""
+ if typ:
+ type = typ
+ if code:
+ cod = code
+ if text:
+ txt = text
+ Node.__init__(self, "error", {}, [Node(name)])
+ if type:
+ self.setAttr("type", type)
+ if not cod:
+ self.setName("stream:error")
+ if txt:
+ self.addChild(node=Node(ns + " text", {}, [txt]))
+ if cod:
+ self.setAttr("code", cod)
+
+class Error(Protocol):
+ """
+ Used to quickly transform received stanza into error reply.
+ """
+ def __init__(self, node, error, reply=1):
+ """
+ Create error reply basing on the received "node" stanza and the "error" error condition.
+ If the "node" is not the received stanza but locally created ("to" and "from" fields needs not swapping)
+ specify the "reply" argument as false.
+ """
+ if reply:
+ Protocol.__init__(self, to=node.getFrom(), frm=node.getTo(), node=node)
+ else:
+ Protocol.__init__(self, node=node)
+ self.setError(error)
+ if node.getType() == "error":
+ self.__str__ = self.__dupstr__
+
+ def __dupstr__(self, dup1=None, dup2=None):
+ """
+ Dummy function used as preventor of creating error node in reply to error node.
+ I.e. you will not be able to serialize "double" error into string.
+ """
+ return ""
+
+class DataField(Node):
+ """
+ This class is used in the DataForm class to describe the single data item.
+ If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122)
+ then you will need to work with instances of this class.
+ """
+ def __init__(self, name=None, value=None, typ=None, required=0, label=None, desc=None, options=[], node=None):
+ """
+ Create new data field of specified name,value and type. Also "required", "desc" and "options" fields can be set.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new datafiled.
+ """
+ Node.__init__(self, "field", node=node)
+ if name:
+ self.setVar(name)
+ if isinstance(value, (list, tuple)):
+ self.setValues(value)
+ elif value:
+ self.setValue(value)
+ if typ:
+ self.setType(typ)
+ elif not typ and not node:
+ self.setType("text-single")
+ if required:
+ self.setRequired(required)
+ if label:
+ self.setLabel(label)
+ if desc:
+ self.setDesc(desc)
+ if options:
+ self.setOptions(options)
+
+ def setRequired(self, req=1):
+ """
+ Change the state of the "required" flag.
+ """
+ if req:
+ self.setTag("required")
+ else:
+ try:
+ self.delChild("required")
+ except ValueError:
+ return None
+
+ def isRequired(self):
+ """
+ Returns in this field a required one.
+ """
+ return self.getTag("required")
+
+ def setLabel(self, label):
+ """
+ Set the label of this field.
+ """
+ self.setAttr("label", label)
+
+ def getLabel(self):
+ """
+ Return the label of this field.
+ """
+ return self.getAttr("label")
+
+ def setDesc(self, desc):
+ """
+ Set the description of this field.
+ """
+ self.setTagData("desc", desc)
+
+ def getDesc(self):
+ """
+ Return the description of this field.
+ """
+ return self.getTagData("desc")
+
+ def setValue(self, val):
+ """
+ Set the value of this field.
+ """
+ self.setTagData("value", val)
+
+ def getValue(self):
+ return self.getTagData("value")
+
+ def setValues(self, ls):
+ """
+ Set the values of this field as values-list.
+ Replaces all previous filed values! If you need to just add a value - use addValue method.
+ """
+ while self.getTag("value"):
+ self.delChild("value")
+ for val in ls:
+ self.addValue(val)
+
+ def addValue(self, val):
+ """
+ Add one more value to this field. Used in "get" iq's or such.
+ """
+ self.addChild("value", {}, [val])
+
+ def getValues(self):
+ """
+ Return the list of values associated with this field.
+ """
+ ret = []
+ for tag in self.getTags("value"):
+ ret.append(tag.getData())
+ return ret
+
+ def getOptions(self):
+ """
+ Return label-option pairs list associated with this field.
+ """
+ ret = []
+ for tag in self.getTags("option"):
+ ret.append([tag.getAttr("label"), tag.getTagData("value")])
+ return ret
+
+ def setOptions(self, ls):
+ """
+ Set label-option pairs list associated with this field.
+ """
+ while self.getTag("option"):
+ self.delChild("option")
+ for opt in ls:
+ self.addOption(opt)
+
+ def addOption(self, opt):
+ """
+ Add one more label-option pair to this field.
+ """
+ if isinstance(opt, basestring):
+ self.addChild("option").setTagData("value", opt)
+ else:
+ self.addChild("option", {"label": opt[0]}).setTagData("value", opt[1])
+
+ def getType(self):
+ """
+ Get type of this field.
+ """
+ return self.getAttr("type")
+
+ def setType(self, val):
+ """
+ Set type of this field.
+ """
+ return self.setAttr("type", val)
+
+ def getVar(self):
+ """
+ Get "var" attribute value of this field.
+ """
+ return self.getAttr("var")
+
+ def setVar(self, val):
+ """
+ Set "var" attribute value of this field.
+ """
+ return self.setAttr("var", val)
+
+class DataReported(Node):
+ """
+ This class is used in the DataForm class to describe the "reported data field" data items which are used in
+ "multiple item form results" (as described in XEP-0004).
+ Represents the fields that will be returned from a search. This information is useful when
+ you try to use the jabber:iq:search namespace to return dynamic form information.
+ """
+ def __init__(self, node=None):
+ """
+ Create new empty "reported data" field. However, note that, according XEP-0004:
+ * It MUST contain one or more DataFields.
+ * Contained DataFields SHOULD possess a "type" and "label" attribute in addition to "var" attribute
+ * Contained DataFields SHOULD NOT contain a <value/> element.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new
+ dataitem.
+ """
+ Node.__init__(self, "reported", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, typ=None, label=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ If created, attributes "type" and "label" are applied to new datafield.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, None, typ, 0, label))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataitem as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
+
+class DataItem(Node):
+ """
+ This class is used in the DataForm class to describe data items which are used in "multiple
+ item form results" (as described in XEP-0004).
+ """
+ def __init__(self, node=None):
+ """
+ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL
+ DataFields described in DataReported.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new
+ dataitem.
+ """
+ Node.__init__(self, "item", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, value=None, typ=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, value, typ))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataitem as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
+
+class DataForm(Node):
+ """
+ DataForm class. Used for manipulating dataforms in XMPP.
+ Relevant XEPs: 0004, 0068, 0122.
+ Can be used in disco, pub-sub and many other applications.
+ """
+ def __init__(self, typ=None, data=[], title=None, node=None):
+ """
+ Create new dataform of type "typ"; "data" is the list of DataReported,
+ DataItem and DataField instances that this dataform contains; "title"
+ is the title string.
+ You can specify the "node" argument as the other node to be used as
+ base for constructing this dataform.
+
+ Title and instructions is optional and SHOULD NOT contain newlines.
+ Several instructions MAY be present.
+ "typ" can be one of ("form" | "submit" | "cancel" | "result" )
+ "typ" of reply iq can be ( "result" | "set" | "set" | "result" ) respectively.
+ "cancel" form can not contain any fields. All other forms contains AT LEAST one field.
+ "title" MAY be included in forms of type "form" and "result".
+ """
+ Node.__init__(self, "x", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ elif n.getName() == "item":
+ newkids.append(DataItem(node=n))
+ elif n.getName() == "reported":
+ newkids.append(DataReported(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+ if typ:
+ self.setType(typ)
+ self.setNamespace(NS_DATA)
+ if title:
+ self.setTitle(title)
+ if isinstance(data, dict):
+ newdata = []
+ for name in data.keys():
+ newdata.append(DataField(name, data[name]))
+ data = newdata
+ for child in data:
+ if isinstance(child, basestring):
+ self.addInstructions(child)
+ elif isinstance(child, DataField):
+ self.kids.append(child)
+ elif isinstance(child, DataItem):
+ self.kids.append(child)
+ elif isinstance(child, DataReported):
+ self.kids.append(child)
+ else:
+ self.kids.append(DataField(node=child))
+
+ def getType(self):
+ """
+ Return the type of dataform.
+ """
+ return self.getAttr("type")
+
+ def setType(self, typ):
+ """
+ Set the type of dataform.
+ """
+ self.setAttr("type", typ)
+
+ def getTitle(self):
+ """
+ Return the title of dataform.
+ """
+ return self.getTagData("title")
+
+ def setTitle(self, text):
+ """
+ Set the title of dataform.
+ """
+ self.setTagData("title", text)
+
+ def getInstructions(self):
+ """
+ Return the instructions of dataform.
+ """
+ return self.getTagData("instructions")
+
+ def setInstructions(self, text):
+ """
+ Set the instructions of dataform.
+ """
+ self.setTagData("instructions", text)
+
+ def addInstructions(self, text):
+ """
+ Add one more instruction to the dataform.
+ """
+ self.addChild("instructions", {}, [text])
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, value=None, typ=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, value, typ))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataform as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
diff --git a/xmpp/roster.py b/xmpp/roster.py index 1cf737f..5a36059 100644 --- a/xmpp/roster.py +++ b/xmpp/roster.py @@ -1,280 +1,280 @@ -## roster.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: roster.py, v1.21 2013/10/21 alkorgun Exp $ - -""" -Simple roster implementation. Can be used though for different tasks like -mass-renaming of contacts. -""" - -from plugin import PlugIn -from protocol import * - -class Roster(PlugIn): - """ - Defines a plenty of methods that will allow you to manage roster. - Also automatically track presences from remote JIDs taking into - account that every JID can have multiple resources connected. Does not - currently support "error" presences. - You can also use mapping interface for access to the internal representation of - contacts in roster. - """ - def __init__(self): - """ - Init internal variables. - """ - PlugIn.__init__(self) - self.DBG_LINE = "roster" - self._data = {} - self.set = None - self._exported_methods = [self.getRoster] - - def plugin(self, owner, request=1): - """ - Register presence and subscription trackers in the owner's dispatcher. - Also request roster from server if the "request" argument is set. - Used internally. - """ - self._owner.RegisterHandler("iq", self.RosterIqHandler, "result", NS_ROSTER) - self._owner.RegisterHandler("iq", self.RosterIqHandler, "set", NS_ROSTER) - self._owner.RegisterHandler("presence", self.PresenceHandler) - if request: - self.Request() - - def Request(self, force=0): - """ - Request roster from server if it were not yet requested - (or if the "force" argument is set). - """ - if self.set is None: - self.set = 0 - elif not force: - return None - self._owner.send(Iq("get", NS_ROSTER)) - self.DEBUG("Roster requested from server", "start") - - def getRoster(self): - """ - Requests roster from server if neccessary and returns self. - """ - if not self.set: - self.Request() - while not self.set: - self._owner.Process(10) - return self - - def RosterIqHandler(self, dis, stanza): - """ - Subscription tracker. Used internally for setting items state in - internal roster representation. - """ - for item in stanza.getTag("query").getTags("item"): - jid = item.getAttr("jid") - if item.getAttr("subscription") == "remove": - if self._data.has_key(jid): - del self._data[jid] - raise NodeProcessed() # a MUST - self.DEBUG("Setting roster item %s..." % jid, "ok") - if jid not in self._data: - self._data[jid] = {} - self._data[jid]["name"] = item.getAttr("name") - self._data[jid]["ask"] = item.getAttr("ask") - self._data[jid]["subscription"] = item.getAttr("subscription") - self._data[jid]["groups"] = [] - if not self._data[jid].has_key("resources"): - self._data[jid]["resources"] = {} - for group in item.getTags("group"): - self._data[jid]["groups"].append(group.getData()) - self._data["@".join((self._owner.User, self._owner.Server))] = {"resources": {}, "name": None, "ask": None, "subscription": None, "groups": None, } - self.set = 1 - raise NodeProcessed() # a MUST. Otherwise you'll get back an <iq type='error'/> - - def PresenceHandler(self, dis, pres): - """ - Presence tracker. Used internally for setting items' resources state in - internal roster representation. - """ - jid = JID(pres.getFrom()) - if not self._data.has_key(jid.getStripped()): - self._data[jid.getStripped()] = {"name": None, "ask": None, "subscription": "none", "groups": ["Not in roster"], "resources": {}} - item = self._data[jid.getStripped()] - typ = pres.getType() - if not typ: - self.DEBUG("Setting roster item %s for resource %s..." % (jid.getStripped(), jid.getResource()), "ok") - item["resources"][jid.getResource()] = res = {"show": None, "status": None, "priority": "0", "timestamp": None} - if pres.getTag("show"): - res["show"] = pres.getShow() - if pres.getTag("status"): - res["status"] = pres.getStatus() - if pres.getTag("priority"): - res["priority"] = pres.getPriority() - if not pres.getTimestamp(): - pres.setTimestamp() - res["timestamp"] = pres.getTimestamp() - elif typ == "unavailable" and item["resources"].has_key(jid.getResource()): - del item["resources"][jid.getResource()] - # Need to handle type="error" also - - def _getItemData(self, jid, dataname): - """ - Return specific jid's representation in internal format. Used internally. - """ - jid = jid[:(jid + "/").find("/")] - return self._data[jid][dataname] - - def _getResourceData(self, jid, dataname): - """ - Return specific jid's resource representation in internal format. Used internally. - """ - if jid.find("/") + 1: - jid, resource = jid.split("/", 1) - if self._data[jid]["resources"].has_key(resource): - return self._data[jid]["resources"][resource][dataname] - elif self._data[jid]["resources"].keys(): - lastpri = -129 - for r in self._data[jid]["resources"].keys(): - if int(self._data[jid]["resources"][r]["priority"]) > lastpri: - resource, lastpri = r, int(self._data[jid]["resources"][r]["priority"]) - return self._data[jid]["resources"][resource][dataname] - - def delItem(self, jid): - """ - Delete contact "jid" from roster. - """ - self._owner.send(Iq("set", NS_ROSTER, payload=[Node("item", {"jid": jid, "subscription": "remove"})])) - - def getAsk(self, jid): - """ - Returns "ask" value of contact "jid". - """ - return self._getItemData(jid, "ask") - - def getGroups(self, jid): - """ - Returns groups list that contact "jid" belongs to. - """ - return self._getItemData(jid, "groups") - - def getName(self, jid): - """ - Returns name of contact "jid". - """ - return self._getItemData(jid, "name") - - def getPriority(self, jid): - """ - Returns priority of contact "jid". "jid" should be a full (not bare) JID. - """ - return self._getResourceData(jid, "priority") - - def getRawRoster(self): - """ - Returns roster representation in internal format. - """ - return self._data - - def getRawItem(self, jid): - """ - Returns roster item "jid" representation in internal format. - """ - return self._data[jid[:(jid + "/").find("/")]] - - def getShow(self, jid): - """ - Returns "show" value of contact "jid". "jid" should be a full (not bare) JID. - """ - return self._getResourceData(jid, "show") - - def getStatus(self, jid): - """ - Returns "status" value of contact "jid". "jid" should be a full (not bare) JID. - """ - return self._getResourceData(jid, "status") - - def getSubscription(self, jid): - """ - Returns "subscription" value of contact "jid". - """ - return self._getItemData(jid, "subscription") - - def getResources(self, jid): - """ - Returns list of connected resources of contact "jid". - """ - return self._data[jid[:(jid + "/").find("/")]]["resources"].keys() - - def setItem(self, jid, name=None, groups=[]): - """ - Creates/renames contact "jid" and sets the groups list that it now belongs to. - """ - iq = Iq("set", NS_ROSTER) - query = iq.getTag("query") - attrs = {"jid": jid} - if name: - attrs["name"] = name - item = query.setTag("item", attrs) - for group in groups: - item.addChild(node=Node("group", payload=[group])) - self._owner.send(iq) - - def getItems(self): - """ - Return list of all [bare] JIDs that the roster is currently tracks. - """ - return self._data.keys() - - def keys(self): - """ - Same as getItems. Provided for the sake of dictionary interface. - """ - return self._data.keys() - - def __getitem__(self, item): - """ - Get the contact in the internal format. Raises KeyError if JID "item" is not in roster. - """ - return self._data[item] - - def getItem(self, item): - """ - Get the contact in the internal format (or None if JID "item" is not in roster). - """ - if self._data.has_key(item): - return self._data[item] - - def Subscribe(self, jid): - """ - Send subscription request to JID "jid". - """ - self._owner.send(Presence(jid, "subscribe")) - - def Unsubscribe(self, jid): - """ - Ask for removing our subscription for JID "jid". - """ - self._owner.send(Presence(jid, "unsubscribe")) - - def Authorize(self, jid): - """ - Authorise JID "jid". Works only if these JID requested auth previously. - """ - self._owner.send(Presence(jid, "subscribed")) - - def Unauthorize(self, jid): - """ - Unauthorise JID "jid". Use for declining authorisation request - or for removing existing authorization. - """ - self._owner.send(Presence(jid, "unsubscribed")) +## roster.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: roster.py, v1.21 2013/10/21 alkorgun Exp $
+
+"""
+Simple roster implementation. Can be used though for different tasks like
+mass-renaming of contacts.
+"""
+
+from .plugin import PlugIn
+from .protocol import *
+
+class Roster(PlugIn):
+ """
+ Defines a plenty of methods that will allow you to manage roster.
+ Also automatically track presences from remote JIDs taking into
+ account that every JID can have multiple resources connected. Does not
+ currently support "error" presences.
+ You can also use mapping interface for access to the internal representation of
+ contacts in roster.
+ """
+ def __init__(self):
+ """
+ Init internal variables.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "roster"
+ self._data = {}
+ self.set = None
+ self._exported_methods = [self.getRoster]
+
+ def plugin(self, owner, request=1):
+ """
+ Register presence and subscription trackers in the owner's dispatcher.
+ Also request roster from server if the "request" argument is set.
+ Used internally.
+ """
+ self._owner.RegisterHandler("iq", self.RosterIqHandler, "result", NS_ROSTER)
+ self._owner.RegisterHandler("iq", self.RosterIqHandler, "set", NS_ROSTER)
+ self._owner.RegisterHandler("presence", self.PresenceHandler)
+ if request:
+ self.Request()
+
+ def Request(self, force=0):
+ """
+ Request roster from server if it were not yet requested
+ (or if the "force" argument is set).
+ """
+ if self.set is None:
+ self.set = 0
+ elif not force:
+ return None
+ self._owner.send(Iq("get", NS_ROSTER))
+ self.DEBUG("Roster requested from server", "start")
+
+ def getRoster(self):
+ """
+ Requests roster from server if neccessary and returns self.
+ """
+ if not self.set:
+ self.Request()
+ while not self.set:
+ self._owner.Process(10)
+ return self
+
+ def RosterIqHandler(self, dis, stanza):
+ """
+ Subscription tracker. Used internally for setting items state in
+ internal roster representation.
+ """
+ for item in stanza.getTag("query").getTags("item"):
+ jid = item.getAttr("jid")
+ if item.getAttr("subscription") == "remove":
+ if jid in self._data:
+ del self._data[jid]
+ raise NodeProcessed() # a MUST
+ self.DEBUG("Setting roster item %s..." % jid, "ok")
+ if jid not in self._data:
+ self._data[jid] = {}
+ self._data[jid]["name"] = item.getAttr("name")
+ self._data[jid]["ask"] = item.getAttr("ask")
+ self._data[jid]["subscription"] = item.getAttr("subscription")
+ self._data[jid]["groups"] = []
+ if "resources" not in self._data[jid]:
+ self._data[jid]["resources"] = {}
+ for group in item.getTags("group"):
+ self._data[jid]["groups"].append(group.getData())
+ self._data["@".join((self._owner.User, self._owner.Server))] = {"resources": {}, "name": None, "ask": None, "subscription": None, "groups": None, }
+ self.set = 1
+ raise NodeProcessed() # a MUST. Otherwise you'll get back an <iq type='error'/>
+
+ def PresenceHandler(self, dis, pres):
+ """
+ Presence tracker. Used internally for setting items' resources state in
+ internal roster representation.
+ """
+ jid = JID(pres.getFrom())
+ if jid.getStripped() not in self._data:
+ self._data[jid.getStripped()] = {"name": None, "ask": None, "subscription": "none", "groups": ["Not in roster"], "resources": {}}
+ item = self._data[jid.getStripped()]
+ typ = pres.getType()
+ if not typ:
+ self.DEBUG("Setting roster item %s for resource %s..." % (jid.getStripped(), jid.getResource()), "ok")
+ item["resources"][jid.getResource()] = res = {"show": None, "status": None, "priority": "0", "timestamp": None}
+ if pres.getTag("show"):
+ res["show"] = pres.getShow()
+ if pres.getTag("status"):
+ res["status"] = pres.getStatus()
+ if pres.getTag("priority"):
+ res["priority"] = pres.getPriority()
+ if not pres.getTimestamp():
+ pres.setTimestamp()
+ res["timestamp"] = pres.getTimestamp()
+ elif typ == "unavailable" and jid.getResource() in item["resources"]:
+ del item["resources"][jid.getResource()]
+ # Need to handle type="error" also
+
+ def _getItemData(self, jid, dataname):
+ """
+ Return specific jid's representation in internal format. Used internally.
+ """
+ jid = jid[:(jid + "/").find("/")]
+ return self._data[jid][dataname]
+
+ def _getResourceData(self, jid, dataname):
+ """
+ Return specific jid's resource representation in internal format. Used internally.
+ """
+ if jid.find("/") + 1:
+ jid, resource = jid.split("/", 1)
+ if resource in self._data[jid]["resources"]:
+ return self._data[jid]["resources"][resource][dataname]
+ elif self._data[jid]["resources"].keys():
+ lastpri = -129
+ for r in self._data[jid]["resources"].keys():
+ if int(self._data[jid]["resources"][r]["priority"]) > lastpri:
+ resource, lastpri = r, int(self._data[jid]["resources"][r]["priority"])
+ return self._data[jid]["resources"][resource][dataname]
+
+ def delItem(self, jid):
+ """
+ Delete contact "jid" from roster.
+ """
+ self._owner.send(Iq("set", NS_ROSTER, payload=[Node("item", {"jid": jid, "subscription": "remove"})]))
+
+ def getAsk(self, jid):
+ """
+ Returns "ask" value of contact "jid".
+ """
+ return self._getItemData(jid, "ask")
+
+ def getGroups(self, jid):
+ """
+ Returns groups list that contact "jid" belongs to.
+ """
+ return self._getItemData(jid, "groups")
+
+ def getName(self, jid):
+ """
+ Returns name of contact "jid".
+ """
+ return self._getItemData(jid, "name")
+
+ def getPriority(self, jid):
+ """
+ Returns priority of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "priority")
+
+ def getRawRoster(self):
+ """
+ Returns roster representation in internal format.
+ """
+ return self._data
+
+ def getRawItem(self, jid):
+ """
+ Returns roster item "jid" representation in internal format.
+ """
+ return self._data[jid[:(jid + "/").find("/")]]
+
+ def getShow(self, jid):
+ """
+ Returns "show" value of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "show")
+
+ def getStatus(self, jid):
+ """
+ Returns "status" value of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "status")
+
+ def getSubscription(self, jid):
+ """
+ Returns "subscription" value of contact "jid".
+ """
+ return self._getItemData(jid, "subscription")
+
+ def getResources(self, jid):
+ """
+ Returns list of connected resources of contact "jid".
+ """
+ return self._data[jid[:(jid + "/").find("/")]]["resources"].keys()
+
+ def setItem(self, jid, name=None, groups=[]):
+ """
+ Creates/renames contact "jid" and sets the groups list that it now belongs to.
+ """
+ iq = Iq("set", NS_ROSTER)
+ query = iq.getTag("query")
+ attrs = {"jid": jid}
+ if name:
+ attrs["name"] = name
+ item = query.setTag("item", attrs)
+ for group in groups:
+ item.addChild(node=Node("group", payload=[group]))
+ self._owner.send(iq)
+
+ def getItems(self):
+ """
+ Return list of all [bare] JIDs that the roster is currently tracks.
+ """
+ return self._data.keys()
+
+ def keys(self):
+ """
+ Same as getItems. Provided for the sake of dictionary interface.
+ """
+ return self._data.keys()
+
+ def __getitem__(self, item):
+ """
+ Get the contact in the internal format. Raises KeyError if JID "item" is not in roster.
+ """
+ return self._data[item]
+
+ def getItem(self, item):
+ """
+ Get the contact in the internal format (or None if JID "item" is not in roster).
+ """
+ if item in self._data:
+ return self._data[item]
+
+ def Subscribe(self, jid):
+ """
+ Send subscription request to JID "jid".
+ """
+ self._owner.send(Presence(jid, "subscribe"))
+
+ def Unsubscribe(self, jid):
+ """
+ Ask for removing our subscription for JID "jid".
+ """
+ self._owner.send(Presence(jid, "unsubscribe"))
+
+ def Authorize(self, jid):
+ """
+ Authorise JID "jid". Works only if these JID requested auth previously.
+ """
+ self._owner.send(Presence(jid, "subscribed"))
+
+ def Unauthorize(self, jid):
+ """
+ Unauthorise JID "jid". Use for declining authorisation request
+ or for removing existing authorization.
+ """
+ self._owner.send(Presence(jid, "unsubscribed"))
diff --git a/xmpp/transports.py b/xmpp/transports.py index 2dd247d..47aa50d 100644 --- a/xmpp/transports.py +++ b/xmpp/transports.py @@ -1,427 +1,427 @@ -## transports.py -## -## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: transports.py, v1.37 2014/01/15 alkorgun Exp $ - -""" -This module contains the low-level implementations of xmpppy connect methods or -(in other words) transports for xmpp-stanzas. -Currently here is three transports: -direct TCP connect - TCPsocket class -proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies) -TLS connection - TLS class. Can be used for SSL connections also. - -Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport. - -Also exception 'error' is defined to allow capture of this module specific exceptions. -""" - -import sys -import socket -if sys.hexversion >= 0x20600F0: - import ssl -import dispatcher - -from base64 import encodestring -from select import select -from simplexml import ustr -from plugin import PlugIn -from protocol import * - -# http://pydns.sourceforge.net -try: - import dns -except ImportError: - dns = None - -DATA_RECEIVED = 'DATA RECEIVED' -DATA_SENT = 'DATA SENT' -DBG_CONNECT_PROXY = 'CONNECTproxy' - -BUFLEN = 1024 - -class error: - """ - An exception to be raised in case of low-level errors in methods of 'transports' module. - """ - def __init__(self, comment): - """ - Cache the descriptive string. - """ - self._comment = comment - - def __str__(self): - """ - Serialize exception into pre-cached descriptive string. - """ - return self._comment - -class TCPsocket(PlugIn): - """ - This class defines direct TCP connection method. - """ - def __init__(self, server=None, use_srv=True): - """ - Cache connection point 'server'. 'server' is the tuple of (host, port) - absolutely the same as standard tcp socket uses. However library will lookup for - ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is) - server instead. - """ - PlugIn.__init__(self) - self.DBG_LINE = "socket" - self._exported_methods = [self.send, self.disconnect] - self._server, self.use_srv = server, use_srv - self._send_queue = [] - - def srv_lookup(self, server): - """ - SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair. - """ - if dns: - query = "_xmpp-client._tcp.%s" % server[0] - try: - dns.DiscoverNameServers() - dns__ = dns.Request() - response = dns__.req(query, qtype="SRV") - if response.answers: - # Sort by priority, according to RFC 2782. - answers = sorted(response.answers, key=lambda a: a["data"][0]) - (port, host) = answers[0]["data"][2:] - server = str(host), int(port) - except dns.DNSError: - self.DEBUG("An error occurred while looking up %s." % query, "warn") - return server - - def plugin(self, owner): - """ - Fire up connection. Return non-empty string on success. - Also registers self.disconnected method in the owner's dispatcher. - Called internally. - """ - if not self._server: - self._server = (self._owner.Server, 5222) - if self.use_srv: - server = self.srv_lookup(self._server) - else: - server = self._server - if not self.connect(server): - return None - self._owner.Connection = self - self._owner.RegisterDisconnectHandler(self.disconnected) - return "ok" - - def getHost(self): - """ - Returns the 'host' value that is connection is [will be] made to. - """ - return self._server[0] - - def getPort(self): - """ - Returns the 'port' value that is connection is [will be] made to. - """ - return self._server[1] - - def connect(self, server=None): - """ - Try to connect to the given host/port. - Returns non-empty string on success. - """ - if not server: - server = self._server - host, port = server - socktype = socket.SOCK_STREAM - try: - lookup = reversed(socket.getaddrinfo(host, int(port), 0, socktype)) - except Exception: - addr = (host, int(port)) - if ":" in host: - af = socket.AF_INET6 - addr = addr.__add__((0, 0)) - else: - af = socket.AF_INET - lookup = [(af, socktype, 1, 6, addr)] - for af, socktype, proto, cn, addr in lookup: - try: - self._sock = socket.socket(af, socktype) - self._sock.connect(addr) - self._send = self._sock.sendall - self._recv = self._sock.recv - except socket.error as error: - if getattr(self, "_sock", None): - self._sock.close() - try: - code, error = error - except Exception: - code = -1 - self.DEBUG("Failed to connect to remote host %s: %s (%s)" % (repr(server), error, code), "error") - except Exception: - pass - else: - self.DEBUG("Successfully connected to remote host %s." % repr(server), "start") - return "ok" - - def plugout(self): - """ - Disconnect from the remote server and unregister self.disconnected method from - the owner's dispatcher. - """ - if getattr(self, "_sock", None): - self._sock.close() - if hasattr(self._owner, "Connection"): - del self._owner.Connection - self._owner.UnregisterDisconnectHandler(self.disconnected) - - def receive(self): - """ - Reads all pending incoming data. - In case of disconnection calls owner's disconnected() method and then raises IOError exception. - """ - try: - data = self._recv(BUFLEN) - except socket.sslerror as e: - self._seen_data = 0 - if e[0] in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): - return "" - self.DEBUG("Socket error while receiving data.", "error") - sys.exc_clear() - self._owner.disconnected() - raise IOError("Disconnected!") - except Exception: - data = "" - while self.pending_data(0): - try: - add = self._recv(BUFLEN) - except Exception: - break - if not add: - break - data += add - if data: - self._seen_data = 1 - self.DEBUG(data, "got") - if hasattr(self._owner, "Dispatcher"): - self._owner.Dispatcher.Event("", DATA_RECEIVED, data) - else: - self.DEBUG("Socket error while receiving data.", "error") - sys.exc_clear() - self._owner.disconnected() - raise IOError("Disconnected!") - return data - - def send(self, data): - self._send_queue.append(data) - - def send_now(self, data, timeout=0.002): - """ - Writes raw outgoing data. Blocks until done. - If supplied data is unicode string, encodes it to utf-8 before send. - """ - if isinstance(data, unicode): - data = data.encode("utf-8") - elif not isinstance(data, str): - data = ustr(data).encode("utf-8") - try: - self._send(data) - except Exception: - self.DEBUG("Socket error while sending data.", "error") - self._owner.disconnected() - else: - if not data.strip(): - data = repr(data) - self.DEBUG(data, "sent") - if hasattr(self._owner, "Dispatcher"): - self._owner.Dispatcher.Event("", DATA_SENT, data) - - def pending_data(self, timeout=0): - """ - Returns true if there is a data ready to be read. - """ - return select([self._sock], [], [], timeout)[0] - - def disconnect(self): - """ - Closes the socket. - """ - self.DEBUG("Closing socket.", "stop") - self._sock.close() - - def disconnected(self): - """ - Called when a Network Error or disconnection occurs. - Designed to be overidden. - """ - self.DEBUG("Socket operation failed.", "error") - -class HTTPPROXYsocket(TCPsocket): - """ - HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). - """ - def __init__(self, proxy, server, use_srv=True): - """ - Caches proxy and target addresses. - 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) - and optional keys 'user' and 'password' to use for authentication. - 'server' argument is a tuple of host and port - just like TCPsocket uses. - """ - TCPsocket.__init__(self, server, use_srv) - self.DBG_LINE = DBG_CONNECT_PROXY - self._proxy = proxy - - def plugin(self, owner): - """ - Starts connection. Used interally. Returns non-empty string on success. - """ - owner.debug_flags.append(DBG_CONNECT_PROXY) - return TCPsocket.plugin(self, owner) - - def connect(self, dupe=None): - """ - Starts connection. Connects to proxy, supplies login and password to it - (if were specified while creating instance). Instructs proxy to make - connection to the target server. Returns non-empty sting on success. - """ - if not TCPsocket.connect(self, (self._proxy["host"], self._proxy["port"])): - return None - self.DEBUG("Proxy server contacted, performing authentification.", "start") - connector = [ - "CONNECT %s:%s HTTP/1.0" % self._server, - "Proxy-Connection: Keep-Alive", - "Pragma: no-cache", - "Host: %s:%s" % self._server, - "User-Agent: HTTPPROXYsocket/v0.1" - ] - if "user" in self._proxy and "password" in self._proxy: - credentials = "%s:%s" % (self._proxy["user"], self._proxy["password"]) - credentials = encodestring(credentials).strip() - connector.append("Proxy-Authorization: Basic " + credentials) - connector.append("\r\n") - self.send("\r\n".join(connector)) - try: - reply = self.receive().replace("\r", "") - except IOError: - self.DEBUG("Proxy suddenly disconnected.", "error") - self._owner.disconnected() - return None - try: - proto, code, desc = reply.split("\n")[0].split(" ", 2) - except Exception: - raise error("Invalid proxy reply") - if code != "200": - self.DEBUG("Invalid proxy reply: %s %s %s" % (proto, code, desc), "error") - self._owner.disconnected() - return None - while reply.find("\n\n") == -1: - try: - reply += self.receive().replace("\r", "") - except IOError: - self.DEBUG("Proxy suddenly disconnected.", "error") - self._owner.disconnected() - return None - self.DEBUG("Authentification successfull. Jabber server contacted.", "ok") - return "ok" - - def DEBUG(self, text, severity): - """ - Overwrites DEBUG tag to allow debug output be presented as 'CONNECTproxy'. - """ - return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity) - -class TLS(PlugIn): - """ - TLS connection used to encrypts already estabilished tcp connection. - """ - def PlugIn(self, owner, now=0): - """ - If the 'now' argument is true then starts using encryption immidiatedly. - If 'now' in false then starts encryption as soon as TLS feature is - declared by the server (if it were already declared - it is ok). - """ - if hasattr(owner, "TLS"): - return None - PlugIn.PlugIn(self, owner) - DBG_LINE = "TLS" - if now: - return self._startSSL() - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandlerOnce("features", self.FeaturesHandler, xmlns=NS_STREAMS) - self.starttls = None - - def plugout(self, now=0): - """ - Unregisters TLS handler's from owner's dispatcher. Take note that encription - can not be stopped once started. You can only break the connection and start over. - """ - self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) - self._owner.UnregisterHandler("proceed", self.StartTLSHandler, xmlns=NS_TLS) - self._owner.UnregisterHandler("failure", self.StartTLSHandler, xmlns=NS_TLS) - - def FeaturesHandler(self, conn, feats): - """ - Used to analyse server <features/> tag for TLS support. - If TLS is supported starts the encryption negotiation. Used internally. - """ - if not feats.getTag("starttls", namespace=NS_TLS): - self.DEBUG("TLS unsupported by remote server.", "warn") - return None - self.DEBUG("TLS supported by remote server. Requesting TLS start.", "ok") - self._owner.RegisterHandlerOnce("proceed", self.StartTLSHandler, xmlns=NS_TLS) - self._owner.RegisterHandlerOnce("failure", self.StartTLSHandler, xmlns=NS_TLS) - self._owner.Connection.send("<starttls xmlns=\"%s\"/>" % NS_TLS) - raise NodeProcessed() - - def pending_data(self, timeout=0): - """ - Returns true if there possible is a data ready to be read. - """ - return self._tcpsock._seen_data or select([self._tcpsock._sock], [], [], timeout)[0] - - def _startSSL(self): - tcpsock = self._owner.Connection - if sys.hexversion >= 0x20600F0: - tcpsock._sslObj = ssl.wrap_socket(tcpsock._sock, None, None) - else: - tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) - tcpsock._sslIssuer = tcpsock._sslObj.issuer() - tcpsock._sslServer = tcpsock._sslObj.server() - tcpsock._recv = tcpsock._sslObj.read - tcpsock._send = tcpsock._sslObj.write - tcpsock._seen_data = 1 - self._tcpsock = tcpsock - tcpsock.pending_data = self.pending_data - tcpsock._sock.setblocking(0) - self.starttls = "success" - - def StartTLSHandler(self, conn, starttls): - """ - Handle server reply if TLS is allowed to process. Behaves accordingly. - Used internally. - """ - if starttls.getNamespace() != NS_TLS: - return None - self.starttls = starttls.getName() - if self.starttls == "failure": - self.DEBUG("Got starttls response: " + self.starttls, "error") - return None - self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...", "ok") - self._startSSL() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) +## transports.py
+##
+## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+# $Id: transports.py, v1.37 2014/01/15 alkorgun Exp $
+
+"""
+This module contains the low-level implementations of xmpppy connect methods or
+(in other words) transports for xmpp-stanzas.
+Currently here is three transports:
+direct TCP connect - TCPsocket class
+proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies)
+TLS connection - TLS class. Can be used for SSL connections also.
+
+Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
+
+Also exception 'error' is defined to allow capture of this module specific exceptions.
+"""
+
+import sys
+import socket
+if sys.hexversion >= 0x20600F0:
+ import ssl
+from . import dispatcher
+
+from base64 import encodestring
+from select import select
+from .simplexml import ustr
+from .plugin import PlugIn
+from .protocol import *
+
+# http://pydns.sourceforge.net
+try:
+ import dns
+except ImportError:
+ dns = None
+
+DATA_RECEIVED = 'DATA RECEIVED'
+DATA_SENT = 'DATA SENT'
+DBG_CONNECT_PROXY = 'CONNECTproxy'
+
+BUFLEN = 1024
+
+class error:
+ """
+ An exception to be raised in case of low-level errors in methods of 'transports' module.
+ """
+ def __init__(self, comment):
+ """
+ Cache the descriptive string.
+ """
+ self._comment = comment
+
+ def __str__(self):
+ """
+ Serialize exception into pre-cached descriptive string.
+ """
+ return self._comment
+
+class TCPsocket(PlugIn):
+ """
+ This class defines direct TCP connection method.
+ """
+ def __init__(self, server=None, use_srv=True):
+ """
+ Cache connection point 'server'. 'server' is the tuple of (host, port)
+ absolutely the same as standard tcp socket uses. However library will lookup for
+ ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is)
+ server instead.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "socket"
+ self._exported_methods = [self.send, self.disconnect]
+ self._server, self.use_srv = server, use_srv
+ self._send_queue = []
+
+ def srv_lookup(self, server):
+ """
+ SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair.
+ """
+ if dns:
+ query = "_xmpp-client._tcp.%s" % server[0]
+ try:
+ dns.DiscoverNameServers()
+ dns__ = dns.Request()
+ response = dns__.req(query, qtype="SRV")
+ if response.answers:
+ # Sort by priority, according to RFC 2782.
+ answers = sorted(response.answers, key=lambda a: a["data"][0])
+ (port, host) = answers[0]["data"][2:]
+ server = str(host), int(port)
+ except dns.DNSError:
+ self.DEBUG("An error occurred while looking up %s." % query, "warn")
+ return server
+
+ def plugin(self, owner):
+ """
+ Fire up connection. Return non-empty string on success.
+ Also registers self.disconnected method in the owner's dispatcher.
+ Called internally.
+ """
+ if not self._server:
+ self._server = (self._owner.Server, 5222)
+ if self.use_srv:
+ server = self.srv_lookup(self._server)
+ else:
+ server = self._server
+ if not self.connect(server):
+ return None
+ self._owner.Connection = self
+ self._owner.RegisterDisconnectHandler(self.disconnected)
+ return "ok"
+
+ def getHost(self):
+ """
+ Returns the 'host' value that is connection is [will be] made to.
+ """
+ return self._server[0]
+
+ def getPort(self):
+ """
+ Returns the 'port' value that is connection is [will be] made to.
+ """
+ return self._server[1]
+
+ def connect(self, server=None):
+ """
+ Try to connect to the given host/port.
+ Returns non-empty string on success.
+ """
+ if not server:
+ server = self._server
+ host, port = server
+ socktype = socket.SOCK_STREAM
+ try:
+ lookup = reversed(socket.getaddrinfo(host, int(port), 0, socktype))
+ except Exception:
+ addr = (host, int(port))
+ if ":" in host:
+ af = socket.AF_INET6
+ addr = addr.__add__((0, 0))
+ else:
+ af = socket.AF_INET
+ lookup = [(af, socktype, 1, 6, addr)]
+ for af, socktype, proto, cn, addr in lookup:
+ try:
+ self._sock = socket.socket(af, socktype)
+ self._sock.connect(addr)
+ self._send = self._sock.sendall
+ self._recv = self._sock.recv
+ except socket.error as error:
+ if getattr(self, "_sock", None):
+ self._sock.close()
+ try:
+ code, error = error
+ except Exception:
+ code = -1
+ self.DEBUG("Failed to connect to remote host %s: %s (%s)" % (repr(server), error, code), "error")
+ except Exception:
+ pass
+ else:
+ self.DEBUG("Successfully connected to remote host %s." % repr(server), "start")
+ return "ok"
+
+ def plugout(self):
+ """
+ Disconnect from the remote server and unregister self.disconnected method from
+ the owner's dispatcher.
+ """
+ if getattr(self, "_sock", None):
+ self._sock.close()
+ if hasattr(self._owner, "Connection"):
+ del self._owner.Connection
+ self._owner.UnregisterDisconnectHandler(self.disconnected)
+
+ def receive(self):
+ """
+ Reads all pending incoming data.
+ In case of disconnection calls owner's disconnected() method and then raises IOError exception.
+ """
+ try:
+ data = self._recv(BUFLEN)
+ except socket.sslerror as e:
+ self._seen_data = 0
+ if e[0] in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
+ return ""
+ self.DEBUG("Socket error while receiving data.", "error")
+ sys.exc_clear()
+ self._owner.disconnected()
+ raise IOError("Disconnected!")
+ except Exception:
+ data = ""
+ while self.pending_data(0):
+ try:
+ add = self._recv(BUFLEN)
+ except Exception:
+ break
+ if not add:
+ break
+ data += add
+ if data:
+ self._seen_data = 1
+ self.DEBUG(data, "got")
+ if hasattr(self._owner, "Dispatcher"):
+ self._owner.Dispatcher.Event("", DATA_RECEIVED, data)
+ else:
+ self.DEBUG("Socket error while receiving data.", "error")
+ sys.exc_clear()
+ self._owner.disconnected()
+ raise IOError("Disconnected!")
+ return data
+
+ def send(self, data):
+ self._send_queue.append(data)
+
+ def send_now(self, data, timeout=0.002):
+ """
+ Writes raw outgoing data. Blocks until done.
+ If supplied data is unicode string, encodes it to utf-8 before send.
+ """
+ if isinstance(data, unicode):
+ data = data.encode("utf-8")
+ elif not isinstance(data, str):
+ data = ustr(data).encode("utf-8")
+ try:
+ self._send(data)
+ except Exception:
+ self.DEBUG("Socket error while sending data.", "error")
+ self._owner.disconnected()
+ else:
+ if not data.strip():
+ data = repr(data)
+ self.DEBUG(data, "sent")
+ if hasattr(self._owner, "Dispatcher"):
+ self._owner.Dispatcher.Event("", DATA_SENT, data)
+
+ def pending_data(self, timeout=0):
+ """
+ Returns true if there is a data ready to be read.
+ """
+ return select([self._sock], [], [], timeout)[0]
+
+ def disconnect(self):
+ """
+ Closes the socket.
+ """
+ self.DEBUG("Closing socket.", "stop")
+ self._sock.close()
+
+ def disconnected(self):
+ """
+ Called when a Network Error or disconnection occurs.
+ Designed to be overidden.
+ """
+ self.DEBUG("Socket operation failed.", "error")
+
+class HTTPPROXYsocket(TCPsocket):
+ """
+ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class
+ redefines only connect method. Allows to use HTTP proxies like squid with
+ (optionally) simple authentication (using login and password).
+ """
+ def __init__(self, proxy, server, use_srv=True):
+ """
+ Caches proxy and target addresses.
+ 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address)
+ and optional keys 'user' and 'password' to use for authentication.
+ 'server' argument is a tuple of host and port - just like TCPsocket uses.
+ """
+ TCPsocket.__init__(self, server, use_srv)
+ self.DBG_LINE = DBG_CONNECT_PROXY
+ self._proxy = proxy
+
+ def plugin(self, owner):
+ """
+ Starts connection. Used interally. Returns non-empty string on success.
+ """
+ owner.debug_flags.append(DBG_CONNECT_PROXY)
+ return TCPsocket.plugin(self, owner)
+
+ def connect(self, dupe=None):
+ """
+ Starts connection. Connects to proxy, supplies login and password to it
+ (if were specified while creating instance). Instructs proxy to make
+ connection to the target server. Returns non-empty sting on success.
+ """
+ if not TCPsocket.connect(self, (self._proxy["host"], self._proxy["port"])):
+ return None
+ self.DEBUG("Proxy server contacted, performing authentification.", "start")
+ connector = [
+ "CONNECT %s:%s HTTP/1.0" % self._server,
+ "Proxy-Connection: Keep-Alive",
+ "Pragma: no-cache",
+ "Host: %s:%s" % self._server,
+ "User-Agent: HTTPPROXYsocket/v0.1"
+ ]
+ if "user" in self._proxy and "password" in self._proxy:
+ credentials = "%s:%s" % (self._proxy["user"], self._proxy["password"])
+ credentials = encodestring(credentials).strip()
+ connector.append("Proxy-Authorization: Basic " + credentials)
+ connector.append("\r\n")
+ self.send("\r\n".join(connector))
+ try:
+ reply = self.receive().replace("\r", "")
+ except IOError:
+ self.DEBUG("Proxy suddenly disconnected.", "error")
+ self._owner.disconnected()
+ return None
+ try:
+ proto, code, desc = reply.split("\n")[0].split(" ", 2)
+ except Exception:
+ raise error("Invalid proxy reply")
+ if code != "200":
+ self.DEBUG("Invalid proxy reply: %s %s %s" % (proto, code, desc), "error")
+ self._owner.disconnected()
+ return None
+ while reply.find("\n\n") == -1:
+ try:
+ reply += self.receive().replace("\r", "")
+ except IOError:
+ self.DEBUG("Proxy suddenly disconnected.", "error")
+ self._owner.disconnected()
+ return None
+ self.DEBUG("Authentification successfull. Jabber server contacted.", "ok")
+ return "ok"
+
+ def DEBUG(self, text, severity):
+ """
+ Overwrites DEBUG tag to allow debug output be presented as 'CONNECTproxy'.
+ """
+ return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity)
+
+class TLS(PlugIn):
+ """
+ TLS connection used to encrypts already estabilished tcp connection.
+ """
+ def PlugIn(self, owner, now=0):
+ """
+ If the 'now' argument is true then starts using encryption immidiatedly.
+ If 'now' in false then starts encryption as soon as TLS feature is
+ declared by the server (if it were already declared - it is ok).
+ """
+ if hasattr(owner, "TLS"):
+ return None
+ PlugIn.PlugIn(self, owner)
+ DBG_LINE = "TLS"
+ if now:
+ return self._startSSL()
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandlerOnce("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self.starttls = None
+
+ def plugout(self, now=0):
+ """
+ Unregisters TLS handler's from owner's dispatcher. Take note that encription
+ can not be stopped once started. You can only break the connection and start over.
+ """
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self._owner.UnregisterHandler("proceed", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.UnregisterHandler("failure", self.StartTLSHandler, xmlns=NS_TLS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Used to analyse server <features/> tag for TLS support.
+ If TLS is supported starts the encryption negotiation. Used internally.
+ """
+ if not feats.getTag("starttls", namespace=NS_TLS):
+ self.DEBUG("TLS unsupported by remote server.", "warn")
+ return None
+ self.DEBUG("TLS supported by remote server. Requesting TLS start.", "ok")
+ self._owner.RegisterHandlerOnce("proceed", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.RegisterHandlerOnce("failure", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.Connection.send("<starttls xmlns=\"%s\"/>" % NS_TLS)
+ raise NodeProcessed()
+
+ def pending_data(self, timeout=0):
+ """
+ Returns true if there possible is a data ready to be read.
+ """
+ return self._tcpsock._seen_data or select([self._tcpsock._sock], [], [], timeout)[0]
+
+ def _startSSL(self):
+ tcpsock = self._owner.Connection
+ if sys.hexversion >= 0x20600F0:
+ tcpsock._sslObj = ssl.wrap_socket(tcpsock._sock, None, None)
+ else:
+ tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None)
+ tcpsock._sslIssuer = tcpsock._sslObj.issuer()
+ tcpsock._sslServer = tcpsock._sslObj.server()
+ tcpsock._recv = tcpsock._sslObj.read
+ tcpsock._send = tcpsock._sslObj.write
+ tcpsock._seen_data = 1
+ self._tcpsock = tcpsock
+ tcpsock.pending_data = self.pending_data
+ tcpsock._sock.setblocking(0)
+ self.starttls = "success"
+
+ def StartTLSHandler(self, conn, starttls):
+ """
+ Handle server reply if TLS is allowed to process. Behaves accordingly.
+ Used internally.
+ """
+ if starttls.getNamespace() != NS_TLS:
+ return None
+ self.starttls = starttls.getName()
+ if self.starttls == "failure":
+ self.DEBUG("Got starttls response: " + self.starttls, "error")
+ return None
+ self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...", "ok")
+ self._startSSL()
+ self._owner.Dispatcher.PlugOut()
+ dispatcher.Dispatcher().PlugIn(self._owner)
|