"""This module provides an Agent which acts as a Jabber client. It can connect to a Jabber server, authenticate, and send and receive Jabber messages. Since this is a fairly massive undertaking, this module is structured as four classes. Each one is a base class of the next. JabberStream (subclass of sched.Agent): Knows how to categorize and dispatch incoming Jabber messages. Also a platform for attaching services. This class really exists only for code organization. There is no reason to instantiate a JabberStream. JabberConnect (subclass of JabberStream): Knows how to open a Jabber connection and parse the incoming XML stanzas. (It actually uses subsidiary agents, from the *tcp* and *xmlagent* modules, to do this work.) This class is able to negotiate stream security (SSL/TLS). It also provides the basic methods for sending Jabber messages and handling incoming ones. JabberAuth (subclass of JabberConnect): Knows how to open a Jabber connection and then authenticate as a specific identity. It can do SASL authentication, or old-style (JEP-0078) authentication. This class provides a method for binding a JID resource, but does not use it. (Although note that old-style authentication automatically binds a resource.) JabberAuthResource (subclass of JabberAuth): Knows how to open a Jabber connection, authenticate, and then bind one resource. This is the class you would use in a typical Jabber client application. """ import sys import types import logging import codecs import sha, base64, random, md5 from zymb import sched, tcp, xmlagent import interface, service # Constants representing choices for stream-level security. SECURE_DEFAULT = '' # TLS if port 5222, SSL if port 5223 SECURE_NONE = 'none' # Do not use stream security SECURE_SSL = 'ssl' # Use SSL (deprecated) SECURE_TLS = 'tls' # Use TLS if the server supports it, otherwise no # security # Constants representing choices for SASL authentication. AUTH_NONE = '' AUTH_SASL_PLAIN = 'PLAIN' AUTH_SASL_DIGESTMD5 = 'DIGEST-MD5' class Dispatcher: """Dispatcher: A class which represents code which handles some Jabber stanzas, according to specific criteria. This class is used internally in JabberStream. You should not create Dispatchers directly. Dispatcher(op, args, keywords, agent) -- constructor. The *keywords* is a dict containing a mapping of keyword:string. The keywords are criteria which must match the incoming stanza. If all the criteria match, the dispatcher's *op* is called (with argument list *args*). This may reject the stanza by returning, or accept it by raising StanzaHandled or a StanzaError. Keyword criteria: name: The top-level tag of the stanza. type: The 'type' attribute. xmlns: The 'xmlns' attribute. id: The 'id' attribute. resource: The resource part of the 'to' attribute, interpreted as a JID. If the JID lacks a resource, that is a non-match. If a keyword is present but the stanza lacks the corresponding attribute, that is a non-match. You can also supply the keyword "accept=True". If you do this, the *op* is presumed to accept the stanza, regardless of whether it returns or raises. (This is useful for creating a dispatcher with some instance method which does not end with "raise StanzaHandled".) Public methods: remove() -- delete the dispatcher from its JabberStream. """ def __init__(self, op, args, keys, agent): if (not args): self.op = op else: self.op = sched.Action(op, *args) assert isinstance(agent, JabberStream) self.agent = agent self.autoaccept = keys.pop('accept', False) self.name = keys.pop('name', None) self.typ = keys.pop('type', None) self.xmlns = keys.pop('xmlns', None) self.resource = keys.pop('resource', None) self.id = keys.pop('id', None) if (keys): raise TypeError('invalid keyword argument for dispatcher: ' + ' '.join(keys.keys())) def __repr__(self): st = '' if (self.name): st += " name='" + self.name + "'" if (self.typ): if (type(self.typ) == tuple): st += " type=" + str(self.typ) else: st += " type='" + self.typ + "'" if (self.xmlns): st += " xmlns='" + self.xmlns + "'" if (self.resource): st += " resource='" + self.resource + "'" if (self.id): st += " id='" + self.id + "'" return '' def check(self, stanza): """check(stanza) -> None Check to see if this dispatcher matches the stanza. If the keyword criteria match, try the dispatcher's operation. Returns if the stanza is rejected; raises StanzaHandled or a StanzaError if accepted. """ # Could optimize this by replacing this at init time, based on # the arguments if (self.name): if (self.name != stanza.getname()): return if (self.xmlns): if (self.xmlns != stanza.getnamespace()): return if (self.resource): tostr = stanza.getattr('to') if (not tostr): return pos = tostr.find('/') if (pos < 0): return if (self.resource != tostr[ pos+1 : ]): return if (self.typ): if (type(self.typ) == tuple): if (not (stanza.getattr('type') in self.typ)): return else: if (self.typ != stanza.getattr('type')): return if (self.id): if (self.id != stanza.getattr('id')): return self.op(stanza) if (self.autoaccept): raise interface.StanzaHandled def remove(self): """remove() -> None Delete the dispatcher from its JabberStream. """ if (self.agent): self.agent.deldispatcher(self) class JabberStream(sched.Agent): """JabberStream: A low-level Jabber agent. Knows how to categorize and dispatch incoming Jabber messages. Also a platform for attaching services. This class really exists only for code organization. There is no reason to instantiate a JabberStream, only one of its subclasses. A JabberStream maintains a collection of dispatchers for incoming messages. A dispatcher knows how to handle some messages; it ignores the rest. A stanza gets handled by exactly one dispatcher. (There is currently no ordering of dispatchers. This shouldn't be a problem; in my experience, you never want two different dispatchers handling overlapping sets of messages. Exception: dispatchers with the 'id' keyword are checked first. An 'id' dispatcher is also one-shot; once it accepts a stanza, it removes itself.) A higher-level abstraction is the notion of a service. You can create a service object and attach it to a JabberStream; the service will provides higher-level communication methods. The service is responsible for adding dispatchers to the JabberStream to do its work. The Service class is defined in the *service* module. JabberStream(jid) -- constructor. The JID may be a string or an interface.JID object. If the JID lacks a resource, 'JID/zymb' will be assumed. Agent states and events: None. Public methods: getjid() -- return the agent's JID. addservice(serv) -- attach a service to handle some Jabber protocol. getservice(serv) -- get the attached service of a given name or class. adddispatcher(op, *args, name=None, type=None, xmlns=None, resource=None, id=None, accept=False) -- add a stanza dispatcher with the specified criteria. deldispatcher(disp) -- remove a stanza dispatcher. Internal methods: dispatch(stanza) -- handle an incoming stanza. deferredwrapper(tup, stanza) -- callback used for deferred handlers. senderror(msg, exc) -- stub method for sending an error back to Jabber. generateid() -- create a unique ID for a Jabber message. endjabberstream() -- 'end' state handler. """ logprefix = 'zymb.jabber' classcounter = 0 def __init__(self, jid): sched.Agent.__init__(self) if (type(jid) in [str, unicode]): jid = interface.JID(jid) if (not jid.getresource()): jid.setresource('zymb') self.jid = jid self.waitingids = {} # { id: disp } self.dispatchers = [] self.services = {} self.counter = 0 JabberStream.classcounter += 1 self.classid = 'jc' + str(JabberStream.classcounter) + 's' self.addhandler('end', self.endjabberstream) def __str__(self): ujidstr = unicode(self.jid) (jidstr, dummy) = codecs.getencoder('unicode_escape')(ujidstr) return '<%s %s>' % (self.__class__, jidstr) def getjid(self): """ getjid() -> JID Return the agent's JID. This will be a full JID, with a resource. The resource can change during authentication, because a Jabber server is not guaranteed to give you the resource you asked for. If you didn't provide a resource in the constructor's JID, getjid().getresource() will be 'zymb' before authentication, and whatever the Jabber server provides afterwards. """ return self.jid def addservice(self, serv): """addservice(serv) -> None Attach a service to handle some Jabber protocol. A service is an object which provides some higher-level methods for the JabberStream to use, and also provides the stanza dispatchers to support those methods. The Service base class is defined in the *service* module. Use this method to attach a newly-created Service object to the JabberStream. The service will call adddispatcher() to set up its handlers. You can then call the service's methods. """ serv.attach(self) def getservice(self, val): """getservice(serv) -> Service Get the attached service of a given name or class. The *serv* value can either be a Service subclass, or a string representing the Service (discoverable as ServiceClass.label). """ if (type(val) == types.ClassType and issubclass(val, service.Service)): val = val.label return self.services.get(val) def adddispatcher(self, op, *args, **keys): """adddispatcher(op, *args, name=None, type=None, xmlns=None, resource=None, id=None, accept=False) -> Dispatcher Add a stanza dispatcher with the specified criteria. This is a low-level method for controlling what happens to incoming Jabber stanzas. When a stanza arrives, it is tested against each of the JabberStream's dispatchers. The stanza must match each keyword argument that is given (and not None). If the stanza matches the dispatcher's criteria, then *op* is called (with the stanza as an argument). The *op* callable may do further matching checks. If it decides the stanza does not match, it should simply return. If it accepts the stanza, it should either take action and raise interface.StanzaHandled, or take no action and raise an interface.StanzaError. The JabberStream will continue checking the stanza against dispatchers until one accepts it. If none do, interface.StanzaFeatureNotImplemented is raised. If an exception is raised which is not a StanzaError, it is converted to a StanzaInternalServerError. Keyword criteria: name: The top-level tag of the stanza. type: The 'type' attribute. xmlns: The 'xmlns' attribute. id: The 'id' attribute. resource: The resource part of the 'to' attribute, interpreted as a JID. If the JID lacks a resource, that is a non-match. If a keyword is present but the stanza lacks the corresponding attribute, that is a non-match. A dispatcher added with the "id=*id*" keyword has special meaning. It is tested before all non-id dispatchers. Also, it is a one-shot -- if the dispatcher accepts the stanza, it is immediately removed from the stream. You can also supply the keyword "accept=True". If you do this, the *op* is presumed to accept the stanza, regardless of whether it returns or raises. (This is useful for creating a dispatcher with some instance method which does not end with "raise StanzaHandled".) """ disp = Dispatcher(op, args, keys, self) id = disp.id if (not id): self.dispatchers.insert(0, disp) else: if (self.waitingids.has_key(id)): raise Exception('already waiting on id \'%s\'' % id) self.waitingids[id] = disp return disp def deldispatcher(self, val): """deldispatcher(disp) -> None Remove a stanza dispatcher. The *disp* value may be a dispatcher (as returned by adddispatcher), or the *op* value from a dispatcher, or the *id* value from an id dispatcher. If no dispatcher matches *disp*, this does nothing. """ if (type(val) in [str, unicode]): id = val if (self.waitingids.has_key(id)): self.waitingids.pop(id) return if (isinstance(val, Dispatcher)): if (val.id and self.waitingids.has_key(val.id)): self.waitingids.pop(val.id) if (val in self.dispatchers): self.dispatchers.remove(val) return biglist = (self.dispatchers + self.waitingids.values()) ls = [ disp for disp in biglist if disp.op == val ] for disp in ls: self.deldispatcher(disp) return def endjabberstream(self): """endjabberstream() -- 'end' state handler. Do not call. This just clears out some lists and dicts, on the theory that cleanliness is close to provable correctness. """ self.dispatchers = [] self.waitingids.clear() self.services.clear() def dispatch(self, stanza): """dispatch(stanza) -- internal method to handle an incoming stanza. This method is used as an agent handler by the JabberConnect subclass. You should not call it. """ try: id = stanza.getattr('id') if (id): if (self.waitingids.has_key(id)): disp = self.waitingids.pop(id) disp.check(stanza) # Didn't get handled, so put it back... self.waitingids[id] = disp for disp in self.dispatchers: disp.check(stanza) raise interface.StanzaFeatureNotImplemented('no matching dispatcher') except sched.Deferred, ex: ex.addcontext(self.deferredwrapper, stanza) raise except interface.StanzaHandled, ex: pass except interface.StanzaError, ex: self.senderror(stanza, ex) except Exception, ex: st = str(ex.__class__) + ': ' + str(ex) self.log.error('Uncaught exception in handler', exc_info=True) self.senderror(stanza, interface.StanzaInternalServerError(st)) def deferredwrapper(self, tup, stanza): """deferredwrapper(tup, stanza) -- callback used for deferred handlers. This is used by dispatch(), for the case where a dispatch handler wants to undertake a deferral operation. You should not call it, or even try to understand it. """ try: res = sched.Deferred.extract(tup) except interface.StanzaHandled, ex: pass except interface.StanzaError, ex: self.senderror(stanza, ex) except Exception, ex: st = str(ex.__class__) + ': ' + str(ex) self.log.error('Uncaught exception in handler', exc_info=True) self.senderror(stanza, interface.StanzaInternalServerError(st)) def senderror(self, msg, ex): """senderror(msg, exc) -- stub method for sending an error back to Jabber. This is invoked by dispatcher() when a stanza handler raises a StanzaError. In JabberStream, it is only a stub. The JabberConnect class has a useful implementation. """ self.log.error('unable to send error for <%s>: %s', msg.getname(), ex) def generateid(self): """generateid() -> str Create a unique ID for a Jabber message. The IDs are of the form 'jcXsY', where X is a number unique to the JabberStream, and Y is a number unique within the JabberStream. It seems as good a scheme as any. """ self.counter += 1 return self.classid + str(self.counter) class JabberConnect(JabberStream): """JabberConnect: A medium-level Jabber agent. Knows how to open a Jabber connection and parse the incoming XML stanzas. (It actually uses subsidiary agents, from the *tcp* and *xmlagent* modules, to do this work.) This class is able to negotiate stream security (SSL/TLS). It also provides the basic methods for sending Jabber messages and handling incoming ones. JabberConnect(jid, port=5222, secure=SECURE_DEFAULT, host=None) -- constructor. The JID may be a string or an interface.JID object. If the JID lacks a resource, 'JID/zymb' will be assumed. The *port* specifies the TCP port on the host. If *host* is none, it is inferred from *jid*. The *secure* value specifies a level of stream security: SECURE_DEFAULT: Use SECURE_TLS if port 5222, SECURE_SSL if port 5223. SECURE_NONE: Do not use stream security. SECURE_SSL: Use SSL security (deprecated). SECURE_TLS: Use TLS security, if the server supports it. For most Jabber servers, you can just specify the port, and the SECURE_DEFAULT setting will do the right thing. Agent states and events: state 'start': Initial state. Start the connection and begin parsing XML. Send our XML header. Wait for the XML agent to receive an incoming XML header; then jump to 'gotheader'. state 'restart': Restarting the stream after TLS or SASL succeeds. Same behavior as state 'start'. state 'gotheader': Wait for stanza; then jump to 'streaming'. state 'streaming': Begin TLS if appropriate; otherwise, jump to 'connected'. state 'startingtls': Waiting for TLS reply. If failure, stop the agent. If successful, move the TCP agent into secure mode. When the TCP agent reaches 'secure', the Jabber agent will restart, as required by the Jabber spec. state 'connected': The stream is connected and security is set up as appropriate. You can start doing work. event 'error' (exc, agent): An error was detected -- either in the connection process, or passed up from the TCP or XML layers. (The *agent* indicates where the error originated.) state 'end': The connection is closed. Public methods: send(msg, addid=True, addfrom=True) -- send a stanza. Internal methods: startconnection() -- 'start' state handler. startstream() -- handler for the TCP agent's 'connected' event. dispatch() -- handler for the XML agent's 'stanza' events. checkstreamattrs() -- handler for the XML agent's 'body' event. instreaming() -- 'streaming' state handler. handle_stanza_tlsfailure() -- TLS dispatcher. handle_stanza_tlsproceed() -- TLS dispatcher. restartstreamtls() -- handler for the TCP agent's 'secure' event. endconnect() -- 'end' state handler. handle_stanza_streamerror() -- stream-level error dispatcher. handle_stanza_features() -- features stanza dispatcher. isunanswerable() -- check whether a stanza should generate an error reply. senderror() -- generate an error reply to a message. """ def __init__(self, jid, port=5222, secure=SECURE_DEFAULT, host=None): JabberStream.__init__(self, jid) self.domain = self.jid.getdomain() if (host): self.host = host else: self.host = self.domain self.port = int(port) if (secure == SECURE_DEFAULT): if (port == 5223): secure = SECURE_SSL else: secure = SECURE_TLS self.secure = secure self.parser = None self.parserendaction = None self.conn = None self.xmldoc = None self.streamfeatures = None self.sendingstreamdoc = False self.encodeunicode = codecs.getencoder('utf8') self.encodelatin1 = codecs.getencoder('latin-1') if (self.secure == SECURE_NONE): connectclass = tcp.TCP elif (self.secure == SECURE_SSL): connectclass = tcp.SSL elif (self.secure == SECURE_TLS): connectclass = tcp.TCPSecure else: raise Exception('invalid secure= argument') self.conn = connectclass(self.host, self.port) self.addhandler('start', self.startconnection) self.addhandler('streaming', self.instreaming) self.addhandler('end', self.endconnect) ac = self.conn.addhandler('connected', self.startstream) self.addcleanupaction(ac) ac = self.conn.addhandler('secure', self.restartstreamtls) self.addcleanupaction(ac) # It would be slightly cleaner to have an 'end' handler that # noted the socket's shutdown, so that we could avoid sending # the final . But it's not a big deal. It just # log a warning. ac = self.conn.addhandler('end', self.stop) self.addcleanupaction(ac) self.addhandler('restart', self.startstream) self.parser = xmlagent.XML(self.conn) ac = self.parser.addhandler('end', self.stop) self.parserendaction = ac ac = self.parser.addhandler('body', self.checkstreamattrs) self.addcleanupaction(ac) ac = self.parser.addhandler('stanza', self.dispatch) self.addcleanupaction(ac) ac = self.parser.addhandler('error', self.perform, 'error') self.addcleanupaction(ac) self.adddispatcher(self.handle_stanza_streamerror, name='error') self.adddispatcher(self.handle_stanza_features, name='features') def startconnection(self): """startconnection() -- internal 'start' state handler. Do not call. Start the subsidiary TCP and XML agents. """ self.conn.start() self.parser.start() def startstream(self): """startstream() -- handler for the TCP agent's 'connected' event. Do not call. Build a Jabber XML stream header and send it. Then wait for the server's stream header to arrive. """ self.log.info('starting stream to %s:%s', self.host, self.port) nod = interface.Node('stream:stream') nod.setnamespace('jabber:client') nod.setattr('version', '1.0') nod.setattr('xmlns:stream', interface.NS_JABBER_ORG_STREAMS) nod.setattr('to', self.domain) self.xmldoc = nod initstr = "%s>" % str(nod)[:-2] st = unicode(initstr) (st, dummylen) = self.encodeunicode(st) self.conn.send(st) self.sendingstreamdoc = True # We've sent our stream header. Now wait in the 'start' (or # 'restart') state until the server's stream header comes in. def checkstreamattrs(self, tag, ns, attrs): """checkstreamattrs() -- handler for the XML agent's 'body' event. Do not call. Make sure that the incoming Jabber stream header is legal. (If not, stop.) If this is a modern Jabber server, jump to 'gotheader' to wait for the features stanza; if not, jump to 'streaming'. """ if (tag != 'stream' or ns != interface.NS_JABBER_ORG_STREAMS): self.log.debug('rejecting doc header <%s xmlns=\'%s\'>', tag, ns) if (tag != 'stream'): errtype = 'invalid-xml' errtext = 'xml document tag was ' + tag else: errtype = 'invalid-namespace' errtext = 'xml document namespace was ' + ns self.perform('error', interface.StreamLevelError(errtype, errtext), self) self.stop() return if (attrs.has_key('version')): vers = attrs.get('version') versls = vers.split('.') if (versls and int(versls[0]) >= 1): # Wait for a features stanza self.jump('gotheader') return # We're talking to an old server. Don't wait for features; skip # straight to the streaming state. self.jump('streaming') def instreaming(self): """instreaming() -- 'streaming' state handler. Do not call. The connection is up and we've got the features (if any). Now it's time for stream-level security. It's possible we've already gone round starting TLS and restarting the stream. If so, jump to 'connected'. It's also possible that there is no security, or that TLS is not available, or that we're using SSL and security was started up before Jabber streaming. In those cases, also jump to 'connected'. Otherwise, we start TLS negotiation. Send the request, set up the stanza dispatchers to await the reply, and jump to 'startingtls'. """ if (self.secure != SECURE_TLS): # Either we've been asked to use no security, or SSL was started # several stages ago. We are fully connected. self.jump('connected') return if (self.conn.ssl): # We've already started TLS. We are fully connected. self.jump('connected') return foundtls = False for nod in self.streamfeatures: if (nod.getname() == 'starttls' and nod.getnamespace() == interface.NS_TLS): foundtls = True if (not foundtls): # This server does not support TLS. We are fully connected. self.jump('connected') return self.log.info('requesting tls negotiation') nod = interface.Node('starttls') nod.setnamespace(interface.NS_TLS) self.adddispatcher(self.handle_stanza_tlsproceed, name='proceed') self.adddispatcher(self.handle_stanza_tlsfailure, name='failure') self.send(nod) self.jump('startingtls') def handle_stanza_tlsfailure(self, msg): """handle_stanza_tlsfailure() -- TLS dispatcher. Do not call. TLS was denied. Stop the agent. """ self.log.error('request to start tls was denied') self.perform('error', Exception('request to start tls was denied'), self) self.stop() raise interface.StanzaHandled def handle_stanza_tlsproceed(self, msg): """handle_stanza_tlsproceed() -- TLS dispatcher. Do not call. TLS was accepted. Tell the TCP agent to start socket-level security. When it does, it will jump to the 'secure' state, triggering our restartstreamtls() handler. """ self.log.info('starting tls') self.conn.beginssl() raise interface.StanzaHandled def restartstreamtls(self): """restartstreamtls() -- handler for the TCP agent's 'secure' event. Do not call. The Jabber spec says that once TLS begins, both sides have to chop off their Jabber streams and start new ones. We do this by killing our XML agent and creating a new one, and then jumping to the 'restart' state. That will trigger the startstream() handler. (The second time around, we'll see that TLS is already started, so we'll bypass 'startingtls' and proceed to the 'connected' state.) """ if (self.state != 'startingtls'): self.log.error('connection began ssl from wrong state') self.perform('error', Exception('connection began ssl from wrong state'), self) self.stop() return self.log.info('tls begun successfully -- restarting stream') # We must now redo our initial setup code. I haven't bothered # to abstract it into a separate function, because there's not # that much of it. self.deldispatcher(self.handle_stanza_tlsproceed) self.deldispatcher(self.handle_stanza_tlsfailure) # Delete the old 'end' handler from parser, so that we can stop # parser without killing ourself self.parserendaction.remove() self.parser.stop() self.parser = None self.parserendaction = None self.xmldoc = None self.streamfeatures = None self.sendingstreamdoc = False # Create a new XML parser. (Easier than resetting the old one.) self.parser = xmlagent.XML(self.conn) ac = self.parser.addhandler('end', self.stop) self.parserendaction = ac ac = self.parser.addhandler('body', self.checkstreamattrs) self.addcleanupaction(ac) ac = self.parser.addhandler('stanza', self.dispatch) self.addcleanupaction(ac) self.parser.start() # Now we jump to 'restart'. The handler attached to this is # self.startstream, so we'll send off a new stream header and # everything will proceed after that. self.jump('restart') def endconnect(self): """endconnect() -- 'end' state handler. Do not call. Send the terminator for our Jabber stream, if we're in the middle of it. Then stop the XML and TCP agents. """ if (self.sendingstreamdoc): try: self.conn.send('') except: # ignore send errors pass self.sendingstreamdoc = False self.xmldoc = None self.parser.stop() self.conn.stop() self.parser = None self.conn = None def handle_stanza_streamerror(self, msg): """handle_stanza_streamerror() -- stream-level error dispatcher. Do not call. This responds to stream-level errors. As required by the spec, a stream-level error stops the agent and shuts down the connection. """ errtype = 'undefined-condition' errtext = '' ls = msg.getchildren() for nod in ls: if (nod.getname() == 'text' and nod.getnamespace() == interface.NS_STREAMS): errtext = nod.getdata() elif (nod.getnamespace() == interface.NS_STREAMS): errtype = nod.getname() if (not errtext): errtext = msg.getdata() self.log.warning('Stream-level error: <%s> %s', errtype, errtext) self.perform('error', interface.StreamLevelError(errtype, errtext), self) self.stop() raise interface.StanzaHandled def handle_stanza_features(self, msg): """handle_stanza_features() -- features stanza dispatcher. Do not call. When the features stanza arrives, if we're waiting in the 'gotheader' state, jump to 'streaming'. """ if (not self.streamfeatures): self.streamfeatures = msg.getchildren() if (self.state == 'gotheader'): self.jump('streaming') raise interface.StanzaHandled def send(self, msg, addid=True, addfrom=True): """send(msg, addid=True, addfrom=True) -> id Send a Jabber message. The *msg* must be a properly-formatted interface.Node tree containing the message. If *addid* is True, this generates a unique message ID and adds it. If *addfrom* is True, this sets the 'from' attr to the agent's own JID. These two arguments are ignored if you have already added the relevant attribute ('id' or 'from') to the top level of *msg*. Return the ID of the message, or None if the message goes out without an ID. """ id = msg.getattr('id') if (addid): if (not id): id = self.generateid() msg.setattr('id', id) if (addfrom): fromaddr = msg.getattr('from') if (not fromaddr): msg.setattr('from', unicode(self.jid)) msg.setparent(self.xmldoc) st = unicode(msg) (st, dummylen) = self.encodeunicode(st) msg.remove() self.conn.send(st) return id def isunanswerable(self, msg): """isunanswerable() -- internal method to check whether a stanza should generate an error reply. Do not call. The spec requires that certain kinds of messages never receive errors in reply. Notably, you should never reply to an error with another error. (This safeguards against echo loops.) This tests a message to see if it is unanswerable. It's somewhat hardwired: there's a special rule for 'iq' stanzas, which I have implemented, but I might have missed special rules for other kinds of stanzas. However, I think it covers the important bits. """ if (msg.getname() == 'error'): return True if (msg.getattr('type') == 'error'): return True if (msg.getname() == 'iq' and msg.getattr('type') == 'result'): return True return False def senderror(self, msg, ex): """senderror() -- generate an error reply to a message. Do not call. This is called by the JabberStream base class if a stanza dispatcher raises a StanzaError. (Or if no dispatcher catches a stanza, thus causing StanzaFeatureNotImplemented; or if a dispatcher throws an unknown exception, thus causing StanzaInternalServerError.) This checks to make sure it's legal to reply with an error to the stanza. If so, it assembles a stanza-level Jabber error, and sends it. """ id = msg.getattr('id', '') jidstr = msg.getattr('from') desc = ex.description if (ex.text): desc = ex.text flag = self.isunanswerable(msg) if (flag): self.log.debug('Rejecting <%s id=\'%s\'> from <%s> (no reply): %s: %s', msg.getname(), id, jidstr, ex.errorname, desc) return self.log.debug('Rejecting <%s id=\'%s\'> from <%s>: %s: %s', msg.getname(), id, jidstr, ex.errorname, desc) errmsg = msg.copy() if (jidstr): errmsg.setattr('to', jidstr) errmsg.setattr('type', 'error') dic = { 'type': ex.errortype } if (ex.errorcode): dic['code'] = ex.errorcode errnod = errmsg.setchild('error', attrs=dic) errnod.setchild(ex.errorname, namespace=interface.NS_STANZAS) dic = { 'xml:lang': 'en' } errnod.setchild('text', attrs=dic, namespace=interface.NS_STANZAS) errnod.setchilddata('text', desc) return self.send(errmsg, addid=False) class JabberAuth(JabberConnect): """JabberAuth: A high-level Jabber agent. Knows how to open a Jabber connection and then authenticate as a specific identity. It can do SASL authentication, or old-style (JEP-0078) authentication. This class provides a method for binding a JID resource, but does not use it. (Although note that old-style authentication automatically binds a resource.) JabberAuth(jid, password, port=5222, secure=SECURE_DEFAULT, register=False, host=None) -- constructor. The JID may be a string or an interface.JID object. If the JID lacks a resource, 'JID/zymb' will be assumed. The *password* is used to authenticate. If *register* is true, the agent tries to register a new account (with the given JID and password) before authenticating. (Not all Jabber servers permit autoregistration in this way.) The *port* specifies the TCP port on the host. If *host* is None, it is inferred from *jid*. The *secure* value specifies a level of stream security: SECURE_DEFAULT: Use SECURE_TLS if port 5222, SECURE_SSL if port 5223. SECURE_NONE: Do not use stream security. SECURE_SSL: Use SSL security (deprecated). SECURE_TLS: Use TLS security, if the server supports it. For most Jabber servers, you can just specify the port, and the SECURE_DEFAULT setting will do the right thing. Agent states and events: (See JabberConnect for states used while connecting and negotiating security. In addition to those, JabberAuth uses the following:) state 'start': Initial state. Begin connection process. state 'restart': Restarting the stream after TLS or SASL succeeds. Same behavior as state 'start'. state 'connected': The stream is connected and security is set up as appropriate. Begin authentication. state 'registering': Attempting in-band registration of a new account. state 'authnonsasl': Performing non-SASL authentication. state 'nonsaslwaitfields': Non-SASL authentication; waiting for field list. state 'nonsaslwaitresponse': Non-SASL authentication; waiting for reply. state 'authsasl': Performing SASL authentication. state 'saslwaitchallenge': SASL authentication; waiting for challenges. state 'saslrestarting': SASL authentication; restarting stream. state 'authed': Authentication has succeeded. You can start doing work. event 'bound' (resource): The stream has successfully bound a Jabber resource. If non-SASL authentication occurs, this happens once during authentication. Otherwise, it occurs only when bindresource() is called successfully. event 'error' (exc, agent): An error was detected -- either in the connection process, during authentication, or passed up from the TCP or XML layers. (The *agent* indicates where the error originated.) state 'end': The connection is closed. Public methods: bindresource() -- bind a JID resource to the authenticated stream. Internal methods: beginauth() -- 'connected' state handler. beginnonsasl() -- 'authnonsasl' state handler. handle_stanza_nonsaslfields() -- non-SASL process dispatcher. handle_stanza_nonsaslresponse() -- non-SASL process dispatcher. beginsasl() -- 'authsasl' state handler. handle_stanza_saslchallenge() -- SASL process dispatcher. handle_stanza_saslfailure() -- SASL process dispatcher. handle_stanza_saslsuccess() -- SASL process dispatcher. restartstreamsasl() -- 'saslrestarting' state handler. beginregister() -- 'registering' state handler. handle_stanza_register() -- registration process dispatcher. handle_stanza_registering() -- registration process dispatcher. announceinitialresource() -- 'authed' state handler. handle_stanza_binding() -- binding process dispatcher. """ def __init__(self, jid, password, port=5222, secure=SECURE_DEFAULT, register=False, host=None): JabberConnect.__init__(self, jid, port, secure, host) self.password = password self.authenticated = False if (register == False or register == None): self.autoregister = False else: self.autoregister = True self.registeremail = str(register) self.authresponsecode = None self.initialresourcebound = False self.addhandler('connected', self.beginauth) self.addhandler('registering', self.beginregister) self.addhandler('authnonsasl', self.beginnonsasl) self.addhandler('authsasl', self.beginsasl) self.addhandler('saslrestarting', self.restartstreamsasl) self.addhandler('authed', self.announceinitialresource) def beginauth(self): """beginauth() -- 'connected' state handler. Do not call. It's possible we've just gone round authentication and restarting the stream. If so, jump to 'authed'. If not, we may want to register a new account, in which case jump to 'registering'. If not, it's time to authenticate. Look at the server's features to determine whether to use SASL or non-SASL (JEP-0078) authentication. If SASL, also determine whether to use DIGEST-MD5 or PLAIN. Then jump to the appropriate state. """ if (self.autoregister): # Before we auth, we will try to register a new account. # Whether that process succeeds or fails, it will return # to 'connected' when it finishes. assert (not self.authenticated) self.jump('registering') return if (self.authenticated): # Whoops, this is our second time through the connected state. self.jump('authed') return foundsasl = False authforms = [] for nod in self.streamfeatures: if (nod.getname() == 'mechanisms' and nod.getnamespace() == interface.NS_SASL): foundsasl = True for mech in nod.getchildren(): if (mech.getname() == 'mechanism' and mech.getdata() == AUTH_SASL_PLAIN): authforms.append(AUTH_SASL_PLAIN) if (mech.getname() == 'mechanism' and mech.getdata() == AUTH_SASL_DIGESTMD5): authforms.append(AUTH_SASL_DIGESTMD5) if (AUTH_SASL_DIGESTMD5 in authforms): self.authform = AUTH_SASL_DIGESTMD5 elif (AUTH_SASL_PLAIN in authforms): self.authform = AUTH_SASL_PLAIN else: ex = interface.StanzaNotAuthorized('server does not support DIGEST-MD5 or PLAIN sasl') self.log.error('authentication could not begin: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.stop() return if (not foundsasl): self.jump('authnonsasl') else: self.jump('authsasl') def beginnonsasl(self): """beginnonsasl() -- 'authnonsasl' state handler. Do not call. Begin the non-SASL (JEP-0078) authentication process. Send an stanza; set up a dispatcher to wait for the reply; jump to the 'nonsaslwaitfields' state. """ self.log.info('beginning non-sasl authentication of <%s>', unicode(self.jid)) msg = interface.Node('iq', attrs={'type':'get', 'to':self.domain}) nod = msg.setchild('query', namespace=interface.NS_AUTH) nod.setchilddata('username', self.jid.getnode()) id = self.send(msg, addfrom=False) # Subtlety here: we don't specify xmlns because that's attached # to the query child, not the top-level node. self.adddispatcher(self.handle_stanza_nonsaslfields, name='iq', type=('result','error'), id=id) self.jump('nonsaslwaitfields') def handle_stanza_nonsaslfields(self, msg): """handle_stanza_nonsaslfields() -- non-SASL process dispatcher. Do not call. Accept a query, which should list the fields we need to authenticate: username, resource, and at least one of digest and password. Send an stanza; set up a dispatcher to wait for the reply; jump to the 'nonsaslwaitresponse' state. """ nod = msg.getchild('query') if (not nod or nod.getnamespace() != interface.NS_AUTH): # Not addressed to us return if (msg.getattr('type') != 'result'): # Error response. Authentication has failed. ex = interface.parseerrorstanza(msg) self.log.error('authentication could not begin: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.stop() raise interface.StanzaHandled # The query list should contain 'username', 'resource', and at least # one of 'digest' and 'password'. subnod = nod.getchild('resource') if (not subnod): raise interface.StanzaBadRequest( 'nonsasl fields list lacks ') subnod = nod.getchild('username') if (not subnod): raise interface.StanzaBadRequest( 'nonsasl fields list lacks ') if (subnod.getdata() != self.jid.getnode()): raise interface.StanzaBadRequest( 'nonsasl fields list does not match auth request') if (nod.getchild('digest')): usedigest = True elif (nod.getchild('password')): usedigest = False else: raise interface.StanzaBadRequest( 'nonsasl fields list lacks and ') newmsg = interface.Node('iq', attrs={'type':'set', 'to':self.domain}) newnod = newmsg.setchild('query', namespace=interface.NS_AUTH) newnod.setchilddata('username', self.jid.getnode()) newnod.setchilddata('resource', self.jid.getresource()) if (not usedigest): newnod.setchilddata('password', self.password) else: streamid = self.parser.docattrs.get('id', '') dat = sha.new(streamid+self.password).hexdigest().lower() newnod.setchilddata('digest', dat) id = self.send(newmsg, addfrom=False) self.adddispatcher(self.handle_stanza_nonsaslresponse, name='iq', type=('result','error'), id=id) self.jump('nonsaslwaitresponse') raise interface.StanzaHandled def handle_stanza_nonsaslresponse(self, msg): """handle_stanza_nonsaslresponse() -- non-SASL process dispatcher. Do not call. Accept a response which says whether authentication succeeded. If it failed, stop the agent. If it succeeded, jump to 'authed'. """ if (msg.getattr('type') != 'result'): # Error response. Authentication has failed. We're not going to # go back and ask for another password; just kill this agent. ex = interface.parseerrorstanza(msg) self.log.error('authentication failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.stop() raise interface.StanzaHandled # Non-SASL authing takes care of the initial resource binding. self.initialresourcebound = True # Discard security information, on general principles self.password = None self.jump('authed') raise interface.StanzaHandled def beginsasl(self): """beginsasl() -- 'authsasl' state handler. Do not call. Begin the SASL authentication process. (See RFC 2222, 2595, 2831.) Send an stanza specifying a mechanism. If using the PLAIN mechanism, include the appropriate data. Set up a dispatcher to wait for the reply; jump to the 'saslwaitchallenge' state. """ self.log.info('beginning sasl (%s) authentication of <%s>', self.authform, unicode(self.jid)) self.adddispatcher(self.handle_stanza_saslchallenge, name='challenge', xmlns=interface.NS_SASL) self.adddispatcher(self.handle_stanza_saslfailure, name='failure', xmlns=interface.NS_SASL) self.adddispatcher(self.handle_stanza_saslsuccess, name='success', xmlns=interface.NS_SASL) msg = interface.Node('auth', attrs={ 'mechanism' : self.authform, 'xmlns' : interface.NS_SASL }) if (self.authform == AUTH_SASL_PLAIN): identstr = '%s@%s' % (self.jid.getnode(), self.jid.getdomain()) dat = ('%s\x00%s\x00%s' % (identstr, self.jid.getnode(), self.password)) # UTF8 and then base64 (st, dummylen) = self.encodeunicode(dat) st64 = base64.encodestring(st).replace('\n', '') msg.setdata(st64) self.send(msg, addid=False, addfrom=False) self.jump('saslwaitchallenge') def handle_stanza_saslchallenge(self, msg): """handle_stanza_saslchallenge() -- SASL process dispatcher. Do not call. Accept a SASL challenge and build the correct response. If this is a reply to our previous challenge response, check its validity (stopping the agent if it is invalid). """ self.log.debug('got sasl challenge') dat = msg.getdata() chal = {} chalstr = base64.decodestring(dat) ls = chalstr.split(',') for st in ls: pos = st.find('=') if (pos >= 0): key = st[ : pos ] val = st[ pos+1 : ] if (val[0] == val[-1] == '"'): val = val[1: -1] chal[key] = val if (chal.has_key('rspauth')): responsevalue = chal['rspauth'] if (responsevalue.lower() != self.authresponsecode): # Reply with an abort message. ex = interface.StanzaNotAuthorized('server sasl response did not match our challenge') self.log.error('authentication failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) msg = interface.Node('abort', attrs={ 'xmlns' : interface.NS_SASL }) self.send(msg, addid=False, addfrom=False) self.stop() else: # Reply with an empty (success) response. msg = interface.Node('response', attrs={ 'xmlns' : interface.NS_SASL }) self.send(msg, addid=False, addfrom=False) raise interface.StanzaHandled valid = True if (chal.has_key('charset') and chal['charset'] != 'utf-8'): valid = False if (chal.has_key('algorithm') and chal['algorithm'] != 'md5-sess'): valid = False if (chal.has_key('qop') and chal['qop'] != 'auth'): valid = False if (not chal.has_key('nonce')): valid = False if (not valid): ex = interface.StanzaNotAuthorized('sasl challenge contained incorrect fields') self.log.error('authentication could not begin: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.stop() raise ex nonce = chal['nonce'] cnonce = [ hex(0x10000+random.randrange(0xFFFF))[-4:] for ix in range(8) ] cnonce = ''.join(cnonce) cnonce = cnonce.lower() noncecount = ('%08x' % 1).lower() jidpass = u'%s:%s:%s' % (self.jid.getnode(), self.jid.getdomain(), self.password) # We want to UTF-8-encode this *unless* it all fits in 8859-1, in # which case we want to 8859-1-encode it. try: (jidpassstr, dummylen) = self.encodelatin1(jidpass) except UnicodeEncodeError: (jidpassstr, dummylen) = self.encodeunicode(jidpass) ### Actually, contrary the SASL spec, Jabber servers seem to want # you to *always* use UTF-8. I posted a query on the Jabber # mailing list about this; Peter St Andre said he'd look into it. # For the moment, we will use UTF-8, because that's what works. (jidpassstr, dummylen) = self.encodeunicode(jidpass) ls = [ encoderawdigest(jidpassstr), nonce, cnonce ] A1 = ':'.join(ls) A2 = 'AUTHENTICATE:xmpp/' A2R = ':xmpp/' ls = [ nonce, noncecount, cnonce, 'auth', encodehexdigest(A2R) ] responserhs = (':'.join(ls)) responsevalue = encodehexkd(encodehexdigest(A1), responserhs) self.authresponsecode = responsevalue ls = [ nonce, noncecount, cnonce, 'auth', encodehexdigest(A2) ] responserhs = (':'.join(ls)) responsevalue = encodehexkd(encodehexdigest(A1), responserhs) resp = [] resp.append('username="%s"' % qdstrencode(self.jid.getnode())) resp.append('realm="%s"' % qdstrencode(self.jid.getdomain())) resp.append('nonce="%s"' % nonce) resp.append('cnonce="%s"' % cnonce) resp.append('nc=%s' % noncecount) resp.append('qop=auth') resp.append('digest-uri="xmpp/"') resp.append('response=%s' % responsevalue) resp.append('charset=utf-8') respstr = (','.join(resp)) (respstr, dummylen) = self.encodeunicode(respstr) resp64 = base64.encodestring(respstr).replace('\n', '') msg = interface.Node('response', attrs={ 'xmlns' : interface.NS_SASL }) msg.setdata(resp64) self.send(msg, addid=False, addfrom=False) raise interface.StanzaHandled def handle_stanza_saslfailure(self, msg): """handle_stanza_saslfailure() -- SASL process dispatcher. Do not call. Accept a message saying that authentication has failed. Stop the agent. """ errtype = 'error' errtext = 'sasl authentication failed' ls = msg.getchildren() if (ls): errtype = ls[0].getname() self.log.error('sasl authentication failed: <%s>', errtype) self.perform('error', SASLError(errtype, errtext), self) self.stop() raise interface.StanzaHandled def handle_stanza_saslsuccess(self, msg): """handle_stanza_saslsuccess() -- SASL process dispatcher. Do not call. Accept a message saying that authentication succeeded. Jump to state 'saslrestarting'. """ if (self.state != 'saslwaitchallenge'): self.log.error('got sasl stanza from wrong state') self.perform('error', Exception('got sasl stanza from wrong state'), self) self.stop() raise interface.StanzaHandled self.jump('saslrestarting') raise interface.StanzaHandled def restartstreamsasl(self): """restartstreamsasl() -- 'saslrestarting' state handler. Do not call. The Jabber spec says that once SASL succeeds, both sides have to chop off their Jabber streams and start new ones. We do this by killing our XML agent and creating a new one, and then jumping to the 'restart' state. That will trigger the startstream() handler. (The second time around, we'll see that we're already authenticated, so we'll bypass authing and proceed to the 'authed' state.) """ self.log.info('sasl authenticated successfully -- restarting stream') self.authenticated = True # Discard security information, on general principles self.password = None self.authresponsecode = None # We must now redo our initial setup code. I haven't bothered # to abstract it into a separate function, because there's not # that much of it. self.deldispatcher(self.handle_stanza_saslchallenge) self.deldispatcher(self.handle_stanza_saslfailure) self.deldispatcher(self.handle_stanza_saslsuccess) # Delete the old 'end' handler from parser, so that we can stop # parser without killing ourself self.parserendaction.remove() self.parser.stop() self.parser = None self.parserendaction = None self.xmldoc = None self.streamfeatures = None self.sendingstreamdoc = False # Create a new XML parser. (Easier than resetting the old one.) self.parser = xmlagent.XML(self.conn) ac = self.parser.addhandler('end', self.stop) self.parserendaction = ac ac = self.parser.addhandler('body', self.checkstreamattrs) self.addcleanupaction(ac) ac = self.parser.addhandler('stanza', self.dispatch) self.addcleanupaction(ac) self.parser.start() # Now we jump to 'restart'. The handler attached to this is # self.startstream, so we'll send off a new stream header and # everything will proceed after that. self.jump('restart') def beginregister(self): """beginregister() -- 'registering' state handler. Do not call. Begin the in-band registration process (JEP-0077). Send a stanza; set up a dispatcher to wait for a reply. Note that if there is an error in registration, we do not stop the agent. We just jump back to the 'connected' state, to try to authenticate anyhow. """ msg = interface.Node('iq', attrs={'type':'get'}) msg.setchild('query', namespace=interface.NS_REGISTER) id = self.send(msg, addfrom=False) self.adddispatcher(self.handle_stanza_register, name='iq', type=('result','error'), id=id) def handle_stanza_register(self, msg): """handle_stanza_register() -- registration process dispatcher. Do not call. Accept a message saying what fields are needed to register. If it indicates an error, or that we're already registered, jump to 'connected'. Otherwise, send an appropriate response. """ if (msg.getattr('type') != 'result'): # Error response. Registration has failed. ex = interface.parseerrorstanza(msg) self.log.error('registration failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.autoregister = False self.jump('connected') raise interface.StanzaHandled nod = msg.getchild('query') if (not nod or nod.getnamespace() != interface.NS_REGISTER): # Not addressed to us return if (nod.getchild('registered')): # Already registered at this JID self.log.warning('unable to register; this jid already registered') self.autoregister = False self.jump('connected') raise interface.StanzaHandled # Send the registration message. newmsg = interface.Node('iq', attrs={'type':'set'}) newnod = newmsg.setchild('query', namespace=interface.NS_REGISTER) newnod.setchild('username').setdata(self.jid.getnode()) newnod.setchild('password').setdata(self.password) if (nod.getchild('email')): newnod.setchild('email').setdata(self.registeremail) id = self.send(newmsg, addfrom=False) self.adddispatcher(self.handle_stanza_registering, name='iq', type=('result','error'), id=id) raise interface.StanzaHandled def handle_stanza_registering(self, msg): """handle_stanza_registering() -- registration process dispatcher. Do not call. Accept a message saying whether registration succeeded or failed. Either way, jump to 'connected'. """ if (msg.getattr('type') != 'result'): # Error response. Registration has failed. ex = interface.parseerrorstanza(msg) self.log.error('registration failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.autoregister = False self.jump('connected') raise interface.StanzaHandled self.log.info('registered successfully -- continuing with auth') self.autoregister = False self.jump('connected') raise interface.StanzaHandled def announceinitialresource(self): """announceinitialresource() -- 'authed' state handler. Do not call. If we used non-SASL authentication, we automatically bound a resource. In that case, we want to perform a 'bound' event after we reach state 'authed'. """ if (self.initialresourcebound): self.perform('bound', self.jid.getresource()) def bindresource(self, resource=None): """bindresource(resource=None) -> None Bind a JID resource to the authenticated stream. If no *resource* is provided, use the resource which was provided in the *jid* of the stream constructor. (If that JID lacked a resource, ask for 'zymb'.) If this succeeds, the agent performs a 'bound' event containing the resource string. Which may not be the one you asked for. If it is different, self.jid is updated with the new resource. (Warning: you can call bindresource() more than once. However, the high-level Jabber features in zymb are not multiple-resource aware. They assume that there is just one resource per stream.) """ if (not resource): resource = self.jid.getresource() msg = interface.Node('iq', attrs={'type':'set'}) nod = msg.setchild('bind', namespace=interface.NS_BIND) nod.setchild('resource').setdata(resource) id = self.send(msg, addfrom=False) self.adddispatcher(self.handle_stanza_binding, name='iq', type=('result','error'), id=id) def handle_stanza_binding(self, msg): """handle_stanza_binding() -- binding process dispatcher. Do not call. Accept a message saying whether binding succeeded. If it did, perform a 'bound' event. """ if (msg.getattr('type') != 'result'): # Error response. Binding has failed. ex = interface.parseerrorstanza(msg) self.log.error('binding failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) raise interface.StanzaHandled nod = msg.getchild('bind') if (not nod or nod.getnamespace() != interface.NS_BIND): # Not addressed to us return jidnod = nod.getchild('jid') jidstr = jidnod.getdata() jid = interface.JID(jidstr) res = jid.getresource() if (res != self.jid.getresource()): # This is a different resource than we asked for! self.jid.setresource(res) self.log.info('new resource; JID is now <%s>', unicode(self.jid)) self.perform('bound', res) raise interface.StanzaHandled class JabberAuthResource(JabberAuth): """JabberAuthResource: A high-level Jabber agent. Knows how to open a Jabber connection, authenticate, and then bind one resource. This is the class you would use in a typical Jabber client application -- one which communicates under a single full JID. When you start the agent, it will connect, authenticate, and bind the resource. Then your application can begin doing whatever it does. JabberAuthResource(jid, password, port=5222, secure=SECURE_DEFAULT, register=False, host=None) -- constructor. The JID may be a string or an interface.JID object. If the JID lacks a resource, 'JID/zymb' will be assumed. The *password* is used to authenticate. If *register* is true, the agent tries to register a new account (with the given JID and password) before authenticating. (Not all Jabber servers permit autoregistration in this way.) The *port* specifies the TCP port on the host. If *host* is None, it is inferred from *jid*. The *secure* value specifies a level of stream security: SECURE_DEFAULT: Use SECURE_TLS if port 5222, SECURE_SSL if port 5223. SECURE_NONE: Do not use stream security. SECURE_SSL: Use SSL security (deprecated). SECURE_TLS: Use TLS security, if the server supports it. For most Jabber servers, you can just specify the port, and the SECURE_DEFAULT setting will do the right thing. Agent states and events: (See JabberAuth for states used while connecting and authenticating. In addition to those, JabberAuthResource uses the following:) state 'start': Initial state. Begin connection process. state 'authstartsession': The stream has authenticated and bound its resource, but has not yet established a session. state 'authresource': The stream has authenticated, bound its resource, and (if necessary) established a session. You can start doing work. event 'error' (exc, agent): An error was detected -- either in the connection process, during authentication, or passed up from the TCP or XML layers. (The *agent* indicates where the error originated.) state 'end': The connection is closed. Internal methods: bindinitialresource() -- 'authed' state handler. detectinitialbinding() -- 'bound' event handler. startinitialsession() -- 'authstartsession' event handler. """ def __init__(self, jid, password, port=5222, secure=SECURE_DEFAULT, register=False, host=None): JabberAuth.__init__(self, jid, password, port, secure, register, host) self.addhandler('authed', self.bindinitialresource) self.addhandler('bound', self.detectinitialbinding) self.addhandler('authstartsession', self.startinitialsession) def bindinitialresource(self): """ bindinitialresource() -- 'authed' state handler. Do not call. This handler ensures that we bind our first resource as soon as authentication is done. If non-SASL authentication was used, then the first resource was bound automatically, so this does nothing. """ if (not self.initialresourcebound): self.bindresource() def detectinitialbinding(self, resource): """detectinitialbinding() -- 'bound' event handler. Do not call. As soon as the first resource is bound, this moves out of the 'authed' state. If the server supports (and requires) sessions, we go to state 'authstartsession'. If not, jump straight to 'authresource'. """ if (self.state == 'authed'): foundsession = False for nod in self.streamfeatures: if (nod.getname() == 'session' and nod.getnamespace() == interface.NS_SESSION): foundsession = True if (not foundsession): self.jump('authresource') else: self.jump('authstartsession') def startinitialsession(self): """startinitialsession() -- 'authstartsession' event handler. Do not call. This establishes an XMPP session, as per RFC-3921. """ hostname = self.jid.getdomain() msg = interface.Node('iq', attrs={'type':'set', 'to':hostname}) msg.setchild('session', namespace=interface.NS_SESSION) id = self.send(msg, addfrom=False) self.adddispatcher(self.handle_stanza_session, name='iq', type=('result','error'), id=id) def handle_stanza_session(self, msg): """handle_stanza_session() -- session process dispatcher. Do not call. Accept a message saying whether session began. If it did, jump to 'authresource'. """ if (msg.getattr('type') != 'result'): # Error response. Session has failed. ex = interface.parseerrorstanza(msg) self.log.error('session failed: <%s> %s', ex.errorname, ex.text) self.perform('error', ex, self) self.stop() raise interface.StanzaHandled self.jump('authresource') raise interface.StanzaHandled class SASLError(Exception): """SASLError: A generic exception used whenever something goes wrong during SASL authentication. """ pass def encoderawdigest(st): """encoderawdigest(str) -> str A helper function used during SASL authentication. Returns the MD5 digest of a string, in the form of a 16-byte string. The bytes can be any value from \x00 to \xff. """ return md5.new(st).digest() def encodehexdigest(st): """encodehexdigest(str) -> str A helper function used during SASL authentication. Returns the MD5 digest of a string, in the form of a 32-character string. The string represents 16 bytes, in lower-case hexadecimal. """ return md5.new(st).hexdigest().lower() def encodehexkd(key, st): """encodehexkd(str1, str2) -> str A helper function used during SASL authentication. Equivalent to encodehexdigest(st), where st = (str1 + ':' + str2). """ return encodehexdigest(key + ':' + st) def qdstrencode(st): """qdstrencode(str) -> str A helper function used during SASL authentication. Quotes strings by backslash-escaping backslashes and double-quote characters. """ return st.replace('\\', '\\\\').replace('"', '\\"')