diff options
author | Yann Leboulanger <asterix@lagaule.org> | 2011-12-29 14:39:02 +0400 |
---|---|---|
committer | Yann Leboulanger <asterix@lagaule.org> | 2011-12-29 14:39:02 +0400 |
commit | 49bc20242163746cefe89c9d8107b0308676f7b7 (patch) | |
tree | 89878c2055358638550c3e7c40295727abe9cb67 /src/common/xmpp | |
parent | c491bd3f59a1bfa442644b722abe3386cfb8f038 (diff) | |
parent | d621252a0d01f63dbbdcc91a0773ae0bd1532af1 (diff) |
merge from trunk
Diffstat (limited to 'src/common/xmpp')
-rw-r--r-- | src/common/xmpp/__init__.py | 1 | ||||
-rw-r--r-- | src/common/xmpp/auth_nb.py | 186 | ||||
-rw-r--r-- | src/common/xmpp/bosh.py | 18 | ||||
-rw-r--r-- | src/common/xmpp/client_nb.py | 13 | ||||
-rw-r--r-- | src/common/xmpp/dispatcher_nb.py | 40 | ||||
-rw-r--r-- | src/common/xmpp/protocol.py | 116 | ||||
-rw-r--r-- | src/common/xmpp/roster_nb.py | 16 | ||||
-rw-r--r-- | src/common/xmpp/smacks.py | 130 | ||||
-rw-r--r-- | src/common/xmpp/stringprepare.py | 4 | ||||
-rw-r--r-- | src/common/xmpp/tls_nb.py | 3 |
10 files changed, 420 insertions, 107 deletions
diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py index 4ebc4cfa3..46037a1cb 100644 --- a/src/common/xmpp/__init__.py +++ b/src/common/xmpp/__init__.py @@ -15,3 +15,4 @@ import simplexml, protocol, auth_nb, transports_nb, roster_nb import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors from client_nb import NonBlockingClient from plugin import PlugIn +from smacks import Smacks diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index 670526bf9..1063ed9a7 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -22,8 +22,10 @@ See client_nb.py """ from protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND, NS_AUTH +from protocol import NS_STREAM_MGMT from protocol import Node, NodeProcessed, isResultNode, Iq, Protocol, JID from plugin import PlugIn +from smacks import Smacks import base64 import random import itertools @@ -40,13 +42,14 @@ def H(some): return hashlib.md5(some).digest() def C(some): return ':'.join(some) try: - import kerberos + kerberos = __import__('kerberos') have_kerberos = True except ImportError: have_kerberos = False GSS_STATE_STEP = 0 GSS_STATE_WRAP = 1 +SASL_FAILURE_IN_PROGRESS = 'failure-in-process' SASL_FAILURE = 'failure' SASL_SUCCESS = 'success' SASL_UNSUPPORTED = 'not-supported' @@ -142,7 +145,7 @@ class SASL(PlugIn): elif self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, - self._owner.Dispatcher.Stream.features) + self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: @@ -154,16 +157,16 @@ class SASL(PlugIn): """ if 'features' in self._owner.__dict__: self._owner.UnregisterHandler('features', self.FeaturesHandler, - xmlns=NS_STREAMS) + xmlns=NS_STREAMS) if 'challenge' in self._owner.__dict__: self._owner.UnregisterHandler('challenge', self.SASLHandler, - xmlns=NS_SASL) + xmlns=NS_SASL) if 'failure' in self._owner.__dict__: self._owner.UnregisterHandler('failure', self.SASLHandler, - xmlns=NS_SASL) + xmlns=NS_SASL) if 'success' in self._owner.__dict__: self._owner.UnregisterHandler('success', self.SASLHandler, - xmlns=NS_SASL) + xmlns=NS_SASL) def auth(self): """ @@ -178,12 +181,12 @@ class SASL(PlugIn): elif self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, - self._owner.Dispatcher.Stream.features) + self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self._owner.RegisterHandler('features', - self.FeaturesHandler, xmlns=NS_STREAMS) + self.FeaturesHandler, xmlns=NS_STREAMS) def FeaturesHandler(self, conn, feats): """ @@ -198,7 +201,8 @@ class SASL(PlugIn): 'mechanism'): self.mecs.append(mec.getData()) - self._owner.RegisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL) + 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) self.MechanismHandler() @@ -206,7 +210,8 @@ class SASL(PlugIn): def MechanismHandler(self): if 'ANONYMOUS' in self.mecs and self.username is None: self.mecs.remove('ANONYMOUS') - node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'}) + node = Node('auth', attrs={'xmlns': NS_SASL, + 'mechanism': 'ANONYMOUS'}) self.mechanism = 'ANONYMOUS' self.startsasl = SASL_IN_PROCESS self._owner.send(str(node)) @@ -226,11 +231,11 @@ class SASL(PlugIn): self.mecs.remove('GSSAPI') try: self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \ - self._owner.xmpp_hostname)[1] + self._owner.xmpp_hostname)[1] kerberos.authGSSClientStep(self.gss_vc, '') response = kerberos.authGSSClientResponse(self.gss_vc) - node=Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'GSSAPI'}, - payload=(response or '')) + node=Node('auth', attrs={'xmlns': NS_SASL, + 'mechanism': 'GSSAPI'}, payload=(response or '')) self.mechanism = 'GSSAPI' self.gss_step = GSS_STATE_STEP self.startsasl = SASL_IN_PROCESS @@ -247,7 +252,8 @@ class SASL(PlugIn): raise NodeProcessed if 'DIGEST-MD5' in self.mecs: self.mecs.remove('DIGEST-MD5') - node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'}) + node = Node('auth', attrs={'xmlns': NS_SASL, + 'mechanism': 'DIGEST-MD5'}) self.mechanism = 'DIGEST-MD5' self.startsasl = SASL_IN_PROCESS self._owner.send(str(node)) @@ -258,6 +264,12 @@ class SASL(PlugIn): self._owner._caller.get_password(self.set_password, self.mechanism) self.startsasl = SASL_IN_PROCESS raise NodeProcessed + if 'X-MESSENGER-OAUTH2' in self.mecs: + self.mecs.remove('X-MESSENGER-OAUTH2') + self.mechanism = 'X-MESSENGER-OAUTH2' + self._owner._caller.get_password(self.set_password, self.mechanism) + self.startsasl = SASL_IN_PROCESS + raise NodeProcessed self.startsasl = SASL_FAILURE log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and ' 'PLAIN mecanisms.') @@ -271,36 +283,54 @@ class SASL(PlugIn): """ if challenge.getNamespace() != NS_SASL: return + + def scram_base64(s): + return ''.join(s.encode('base64').split('\n')) + + incoming_data = challenge.getData() + data=base64.decodestring(incoming_data) ### Handle Auth result - if challenge.getName() == 'failure': - self.startsasl = SASL_FAILURE - try: - reason = challenge.getChildren()[0] - except Exception: - reason = challenge + def on_auth_fail(reason): log.info('Failed SASL authentification: %s' % reason) + self._owner.send(str(Node('abort', attrs={'xmlns': NS_SASL}))) if len(self.mecs) > 0: - # There are other mechanisms to test - self.MechanismHandler() + # There are other mechanisms to test, but wait for <failure> + # answer from server + self.startsasl = SASL_FAILURE_IN_PROGRESS raise NodeProcessed if self.on_sasl: self.on_sasl() raise NodeProcessed + + if challenge.getName() == 'failure': + if self.startsasl == SASL_FAILURE_IN_PROGRESS: + self.MechanismHandler() + raise NodeProcessed + self.startsasl = SASL_FAILURE + try: + reason = challenge.getChildren()[0] + except Exception: + reason = challenge + on_auth_fail(reason) elif challenge.getName() == 'success': - # TODO: Need to validate any data-with-success. - # TODO: Important for DIGEST-MD5 and SCRAM. + if self.mechanism == 'SCRAM-SHA-1': + # check data-with-success + data = scram_parse(data) + if data['v'] != scram_base64(self.scram_ServerSignature): + on_auth_fail('ServerSignature is wrong') + self.startsasl = SASL_SUCCESS log.info('Successfully authenticated with remote server.') handlers = self._owner.Dispatcher.dumpHandlers() # Bosh specific dispatcher replugging - # save old features. They will be used in case we won't get response on - # stream restart after SASL auth (happens with XMPP over BOSH with - # Openfire) + # save old features. They will be used in case we won't get response + # on stream restart after SASL auth (happens with XMPP over BOSH + # with Openfire) old_features = self._owner.Dispatcher.Stream.features self._owner.Dispatcher.PlugOut() dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner, - after_SASL=True, old_features=old_features) + after_SASL=True, old_features=old_features) self._owner.Dispatcher.restoreHandlers(handlers) self._owner.User = self.username @@ -309,8 +339,6 @@ class SASL(PlugIn): raise NodeProcessed ### Perform auth step - incoming_data = challenge.getData() - data=base64.decodestring(incoming_data) log.info('Got challenge:' + data) if self.mechanism == 'GSSAPI': @@ -322,12 +350,12 @@ class SASL(PlugIn): rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data) response = kerberos.authGSSClientResponse(self.gss_vc) rc = kerberos.authGSSClientWrap(self.gss_vc, response, - kerberos.authGSSClientUserName(self.gss_vc)) + kerberos.authGSSClientUserName(self.gss_vc)) response = kerberos.authGSSClientResponse(self.gss_vc) if not response: response = '' self._owner.send(Node('response', attrs={'xmlns': NS_SASL}, - payload=response).__str__()) + payload=response).__str__()) raise NodeProcessed if self.mechanism == 'SCRAM-SHA-1': hashfn = hashlib.sha1 @@ -353,12 +381,9 @@ class SASL(PlugIn): ui = XOR(ui, ui_1) return ui - def H(s): + def scram_H(s): return hashfn(s).digest() - def scram_base64(s): - return ''.join(s.encode('base64').split('\n')) - if self.scram_step == 0: self.scram_step = 1 self.scram_soup += ',' + data + ',' @@ -373,7 +398,7 @@ class SASL(PlugIn): SaltedPassword = Hi(self.password, salt, iter) # TODO: Could cache this, along with salt+iter. ClientKey = HMAC(SaltedPassword, 'Client Key') - StoredKey = H(ClientKey) + StoredKey = scram_H(ClientKey) ClientSignature = HMAC(StoredKey, self.scram_soup) ClientProof = XOR(ClientKey, ClientSignature) r += ',p=' + scram_base64(ClientProof) @@ -408,8 +433,8 @@ class SASL(PlugIn): else: self.resp['realm'] = self._owner.Server self.resp['nonce'] = chal['nonce'] - self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint in - itertools.repeat(random.randint, 7)) + self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint \ + in itertools.repeat(random.randint, 7)) self.resp['nc'] = ('00000001') self.resp['qop'] = 'auth' self.resp['digest-uri'] = 'xmpp/' + self._owner.Server @@ -417,6 +442,9 @@ class SASL(PlugIn): # Password is now required self._owner._caller.get_password(self.set_password, self.mechanism) elif 'rspauth' in chal: + # Check rspauth value + if chal['rspauth'] != self.digest_rspauth: + on_auth_fail('rspauth is wrong') self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL}))) else: self.startsasl = SASL_FAILURE @@ -449,10 +477,14 @@ class SASL(PlugIn): hash_realm = self._convert_to_iso88591(self.resp['realm']) hash_password = self._convert_to_iso88591(self.password) A1 = C([H(C([hash_username, hash_realm, hash_password])), - self.resp['nonce'], self.resp['cnonce']]) + self.resp['nonce'], self.resp['cnonce']]) A2 = C(['AUTHENTICATE', self.resp['digest-uri']]) - response= HH(C([HH(A1), self.resp['nonce'], self.resp['nc'], - self.resp['cnonce'], self.resp['qop'], HH(A2)])) + response = HH(C([HH(A1), self.resp['nonce'], self.resp['nc'], + self.resp['cnonce'], self.resp['qop'], HH(A2)])) + A2 = C(['', self.resp['digest-uri']]) + self.digest_rspauth = HH(C([HH(A1), self.resp['nonce'], + self.resp['nc'], self.resp['cnonce'], self.resp['qop'], + HH(A2)])) self.resp['response'] = response sasl_data = u'' for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce', @@ -462,14 +494,19 @@ class SASL(PlugIn): else: sasl_data += u'%s="%s",' % (key, self.resp[key]) sasl_data = sasl_data[:-1].encode('utf-8').encode('base64').replace( - '\r', '').replace('\n', '') - node = Node('response', attrs={'xmlns':NS_SASL}, payload=[sasl_data]) + '\r', '').replace('\n', '') + node = Node('response', attrs={'xmlns': NS_SASL}, + payload=[sasl_data]) elif self.mechanism == 'PLAIN': sasl_data = u'\x00%s\x00%s' % (self.username, self.password) sasl_data = sasl_data.encode('utf-8').encode('base64').replace( - '\n', '') + '\n', '') node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'}, - payload=[sasl_data]) + payload=[sasl_data]) + elif self.mechanism == 'X-MESSENGER-OAUTH2': + node = Node('auth', attrs={'xmlns': NS_SASL, + 'mechanism': 'X-MESSENGER-OAUTH2'}) + node.addData(password) self._owner.send(str(node)) @@ -501,8 +538,8 @@ class NonBlockingNonSASL(PlugIn): self.owner = owner owner.Dispatcher.SendAndWaitForResponse( - Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]), - func=self._on_username) + Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]), + func=self._on_username) def _on_username(self, resp): if not isResultNode(resp): @@ -517,8 +554,8 @@ class NonBlockingNonSASL(PlugIn): if query.getTag('digest'): log.info("Performing digest authentication") query.setTagData('digest', - hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id'] - + self.password).hexdigest()) + hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id'] + + self.password).hexdigest()) if query.getTag('password'): query.delChild('password') self._method = 'digest' @@ -533,23 +570,25 @@ class NonBlockingNonSASL(PlugIn): def hash_n_times(s, count): return count and hasher(hash_n_times(s, count-1)) or s - hash_ = hash_n_times(hasher(hasher(self.password) + token), int(seq)) + hash_ = hash_n_times(hasher(hasher(self.password) + token), + int(seq)) query.setTagData('hash', hash_) self._method='0k' else: log.warn("Secure methods unsupported, performing plain text \ - authentication") + authentication") query.setTagData('password', self.password) self._method = 'plain' - resp = self.owner.Dispatcher.SendAndWaitForResponse(iq, func=self._on_auth) + resp = self.owner.Dispatcher.SendAndWaitForResponse(iq, + func=self._on_auth) def _on_auth(self, resp): if isResultNode(resp): log.info('Sucessfully authenticated with remote host.') self.owner.User = self.user self.owner.Resource = self.resource - self.owner._registered_name = self.owner.User+'@'+self.owner.Server+\ - '/'+self.owner.Resource + self.owner._registered_name = self.owner.User + '@' + \ + self.owner.Server+ '/' + self.owner.Resource return self.on_auth(self._method) log.info('Authentication failed!') return self.on_auth(None) @@ -564,24 +603,34 @@ class NonBlockingBind(PlugIn): def __init__(self): PlugIn.__init__(self) self.bound = None + self.supports_sm = False + self.resuming = False 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) + self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self._owner.RegisterHandler('features', self.FeaturesHandler, - xmlns=NS_STREAMS) + xmlns=NS_STREAMS) def FeaturesHandler(self, conn, feats): """ Determine if server supports resource binding and set some internal - attributes accordingly + attributes accordingly. + + It also checks if server supports stream management """ + + if feats.getTag('sm', namespace=NS_STREAM_MGMT): + self.supports_sm = True # server supports stream management + if self.resuming: + self._owner._caller.sm.resume_request() + if not feats.getTag('bind', namespace=NS_BIND): log.info('Server does not requested binding.') # we try to bind resource anyway @@ -599,12 +648,14 @@ class NonBlockingBind(PlugIn): Remove Bind handler from owner's dispatcher. Used internally """ self._owner.UnregisterHandler('features', self.FeaturesHandler, - xmlns=NS_STREAMS) + xmlns=NS_STREAMS) def NonBlockingBind(self, resource=None, on_bound=None): """ Perform binding. Use provided resource name or random (if not provided). """ + if self.resuming: # We don't bind if we resume the stream + return self.on_bound = on_bound self._resource = resource if self._resource: @@ -614,8 +665,9 @@ class NonBlockingBind(PlugIn): self._owner.onreceive(None) self._owner.Dispatcher.SendAndWaitForResponse( - Protocol('iq', typ='set', payload=[Node('bind', attrs={'xmlns':NS_BIND}, - payload=self._resource)]), func=self._on_bound) + Protocol('iq', typ='set', payload=[Node('bind', + attrs={'xmlns': NS_BIND}, payload=self._resource)]), + func=self._on_bound) def _on_bound(self, resp): if isResultNode(resp): @@ -625,14 +677,22 @@ class NonBlockingBind(PlugIn): jid = JID(resp.getTag('bind').getTagData('jid')) self._owner.User = jid.getNode() self._owner.Resource = jid.getResource() + # Only negociate stream management after bounded + sm = self._owner._caller.sm + if self.supports_sm: + # starts negociation + sm.set_owner(self._owner) + sm.negociate() + self._owner.Dispatcher.sm = sm + if hasattr(self, 'session') and self.session == -1: # Server don't want us to initialize a session log.info('No session required.') self.on_bound('ok') else: self._owner.SendAndWaitForResponse(Protocol('iq', typ='set', - payload=[Node('session', attrs={'xmlns':NS_SESSION})]), - func=self._on_session) + payload=[Node('session', attrs={'xmlns':NS_SESSION})]), + func=self._on_session) return if resp: log.info('Binding failed: %s.' % resp.getTag('error')) diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 353e79826..9bf5be7f3 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -413,15 +413,15 @@ class NonBlockingBOSH(NonBlockingTransport): 'xmlns:xmpp': 'urn:xmpp:xbosh'}) else: t = BOSHBody( - attrs={ 'content': self.bosh_content, - 'hold': str(self.bosh_hold), - 'route': '%s:%s' % (self.route_host, self.route_port), - 'to': self.bosh_to, - 'wait': str(self.bosh_wait), - 'xml:lang': self.bosh_xml_lang, - 'xmpp:version': '1.0', - 'ver': '1.6', - 'xmlns:xmpp': 'urn:xmpp:xbosh'}) + attrs={ 'content': self.bosh_content, + 'hold': str(self.bosh_hold), + 'route': 'xmpp:%s:%s' % (self.route_host, self.route_port), + 'to': self.bosh_to, + 'wait': str(self.bosh_wait), + 'xml:lang': self.bosh_xml_lang, + 'xmpp:version': '1.0', + 'ver': '1.6', + 'xmlns:xmpp': 'urn:xmpp:xbosh'}) self.send_BOSH((t, True)) def start_disconnect(self): diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index dc4c4476b..608be0fa6 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -493,6 +493,8 @@ class NonBlockingClient: if self._sasl: auth_nb.SASL.get_instance(self._User, self._Password, self._on_start_sasl).PlugIn(self) + if not hasattr(self, 'SASL'): + return if not self._sasl or self.SASL.startsasl == 'not-supported': if not self._Resource: self._Resource = 'xmpppy' @@ -521,7 +523,16 @@ class NonBlockingClient: self.connected = None # FIXME: is this intended? We use ''elsewhere self._on_sasl_auth(None) elif self.SASL.startsasl == 'success': - auth_nb.NonBlockingBind.get_instance().PlugIn(self) + nb_bind = auth_nb.NonBlockingBind.get_instance() + sm = self._caller.sm + if sm._owner and sm.resumption: + nb_bind.resuming = True + sm.set_owner(self) + self.Dispatcher.sm = sm + nb_bind.PlugIn(self) + return + + nb_bind.PlugIn(self) self.onreceive(self._on_auth_bind) return True diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index cca56f33a..37020e5c6 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -21,6 +21,7 @@ different handlers to different XMPP stanzas and namespaces """ import simplexml, sys, locale +import re from xml.parsers.expat import ExpatError from plugin import PlugIn from protocol import (NS_STREAMS, NS_XMPP_STREAMS, NS_HTTP_BIND, Iq, Presence, @@ -90,6 +91,27 @@ class XMPPDispatcher(PlugIn): self.SendAndWaitForResponse, self.SendAndCallForResponse, self.getAnID, self.Event, self.send] + # Let the dispatcher know if there is support for stream management + self.sm = None + + # \ufddo -> \ufdef range + c = u'\ufdd0' + r = c.encode('utf8') + while (c < u'\ufdef'): + c = unichr(ord(c) + 1) + r += '|' + c.encode('utf8') + + # \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff + c = u'\ufffe' + r += '|' + c.encode('utf8') + r += '|' + unichr(ord(c) + 1).encode('utf8') + while (c < u'\U0010fffe'): + c = unichr(ord(c) + 0x10000) + r += '|' + c.encode('utf8') + r += '|' + unichr(ord(c) + 1).encode('utf8') + + self.invalid_chars_re = re.compile(r) + def getAnID(self): global outgoingID outgoingID += 1 @@ -175,6 +197,9 @@ class XMPPDispatcher(PlugIn): raise ValueError('Incorrect stream start: (%s,%s). Terminating.' % (tag, ns)) + def replace_non_character(self, data): + return re.sub(self.invalid_chars_re, u'\ufffd'.encode('utf-8'), data) + def ProcessNonBlocking(self, data): """ Check incoming stream for data waiting @@ -190,6 +215,7 @@ class XMPPDispatcher(PlugIn): # disconnect method will never be called. # Is this intended? # also look at transports start_disconnect() + data = self.replace_non_character(data) for handler in self._cycleHandlers: handler(self) if len(self._pendingExceptions) > 0: @@ -417,6 +443,12 @@ class XMPPDispatcher(PlugIn): stanza.props = stanza.getProperties() ID = stanza.getID() + # If server supports stream management + if self.sm and self.sm.enabled and (stanza.getName() != 'r' and + stanza.getName() != 'a' and stanza.getName() != 'enabled' and + stanza.getName() != 'resumed'): + # increments the number of stanzas that has been handled + self.sm.in_h = self.sm.in_h + 1 list_ = ['default'] # we will use all handlers: if typ in self.handlers[xmlns][name]: list_.append(typ) # from very common... @@ -525,6 +557,14 @@ class XMPPDispatcher(PlugIn): ID = stanza.getID() if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from', self._owner._registered_name) + + # If no ID then it is a whitespace + if self.sm and self.sm.enabled and ID: + self.sm.uqueue.append(stanza) + self.sm.out_h = self.sm.out_h + 1 + if len(self.sm.uqueue) > self.sm.max_queue: + self.sm.request_ack() + self._owner.Connection.send(stanza, now) return ID diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index f32863674..87bc44ec6 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -47,12 +47,14 @@ NS_BOB = 'urn:xmpp:bob' # XEP-0231 NS_BOOKMARKS = 'storage:bookmarks' # XEP-0048 NS_BROWSE = 'jabber:iq:browse' NS_BROWSING = 'http://jabber.org/protocol/browsing' # XEP-0195 -NS_BYTESTREAM = 'http://jabber.org/protocol/bytestreams' # JEP-0065 -NS_CAPS = 'http://jabber.org/protocol/caps' # JEP-0115 +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' # JEP-0085 +NS_CARBONS = 'urn:xmpp:carbons:1' # XEP-0280 +NS_CHATSTATES = 'http://jabber.org/protocol/chatstates' # XEP-0085 NS_CHATTING = 'http://jabber.org/protocol/chatting' # XEP-0194 NS_CLIENT = 'jabber:client' +NS_CONDITIONS = 'urn:xmpp:muc:conditions:0' # XEP-0306 NS_COMMANDS = 'http://jabber.org/protocol/commands' NS_COMPONENT_ACCEPT = 'jabber:component:accept' NS_COMPONENT_1 = 'http://jabberd.jabberstudio.org/ns/component/1.0' @@ -69,9 +71,10 @@ NS_DISCO_ITEMS = NS_DISCO + '#items' NS_ENCRYPTED = 'jabber:x:encrypted' # XEP-0027 NS_ESESSION = 'http://www.xmpp.org/extensions/xep-0116.html#ns' NS_ESESSION_INIT = 'http://www.xmpp.org/extensions/xep-0116.html#ns-init' # XEP-0116 -NS_EVENT = 'jabber:x:event' # XEP-0022 +NS_EVENT = 'jabber:x:event' # XEP-0022 NS_FEATURE = 'http://jabber.org/protocol/feature-neg' -NS_FILE = 'http://jabber.org/protocol/si/profile/file-transfer' # JEP-0096 +NS_FILE = 'http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096 +NS_FORWARD = 'urn:xmpp:forward:0' # XEP-0297 NS_GAMING = 'http://jabber.org/protocol/gaming' # XEP-0196 NS_GATEWAY = 'jabber:iq:gateway' # XEP-0100 NS_GEOLOC = 'http://jabber.org/protocol/geoloc' # XEP-0080 @@ -105,7 +108,7 @@ NS_MUC_CONFIG = NS_MUC + '#roomconfig' NS_NICK = 'http://jabber.org/protocol/nick' # XEP-0172 NS_OFFLINE = 'http://www.jabber.org/jeps/jep-0030.html' # XEP-0013 NS_PHYSLOC = 'http://jabber.org/protocol/physloc' # XEP-0112 -NS_PING = 'urn:xmpp:ping' # SEP-0199 +NS_PING = 'urn:xmpp:ping' # XEP-0199 NS_PRESENCE = 'presence' # Jabberd2 NS_PRIVACY = 'jabber:iq:privacy' NS_PRIVATE = 'jabber:iq:private' @@ -113,7 +116,7 @@ NS_PROFILE = 'http://jabber.org/protocol/profile' # XEP-0154 NS_PUBSUB = 'http://jabber.org/protocol/pubsub' # XEP-0060 NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options' # XEP-0060 -NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' # JEP-0060 +NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' # XEP-0060 NS_REGISTER = 'jabber:iq:register' NS_ROSTER = 'jabber:iq:roster' NS_ROSTERNOTES = 'storage:rosternotes' @@ -123,7 +126,7 @@ NS_RPC = 'jabber:iq:rpc' # XEP-0009 NS_RSM = 'http://jabber.org/protocol/rsm' NS_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl' NS_SECLABEL = 'urn:xmpp:sec-label:0' -NS_SECLABEL_CATALOG = 'urn:xmpp:sec-label:catalog:0' +NS_SECLABEL_CATALOG = 'urn:xmpp:sec-label:catalog:2' NS_SEARCH = 'jabber:iq:search' NS_SERVER = 'jabber:server' NS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' @@ -153,9 +156,10 @@ NS_DATA_LAYOUT = 'http://jabber.org/protocol/xdata-layout' # XEP-0141 NS_DATA_VALIDATE = 'http://jabber.org/protocol/xdata-validate' # XEP-0122 NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' NS_RECEIPTS = 'urn:xmpp:receipts' -NS_PUBKEY_PUBKEY='urn:xmpp:pubkey:2' # XEP-0189 -NS_PUBKEY_REVOKE='urn:xmpp:revoke:2' -NS_PUBKEY_ATTEST='urn:xmpp:attest:2' +NS_PUBKEY_PUBKEY = 'urn:xmpp:pubkey:2' # XEP-0189 +NS_PUBKEY_REVOKE = 'urn:xmpp:revoke:2' +NS_PUBKEY_ATTEST = 'urn:xmpp:attest:2' +NS_STREAM_MGMT = 'urn:xmpp:sm:2' # XEP-198 xmpp_stream_error_conditions = ''' bad-format -- -- -- The entity has sent XML that cannot be processed. @@ -631,6 +635,17 @@ class Protocol(Node): """ return self.getTagAttr('error', 'code') + def getStatusConditions(self): + """ + Return the status conditions list as defined in XEP-0306. + """ + conds = [] + condtag = self.getTag('conditions', namespace=NS_CONDITIONS) + if condtag: + for tag in condtag.getChildren(): + conds.append(tag.getName()) + return conds + def setError(self, error, code=None): """ Set the error code. Obsolete. Use error-conditions instead @@ -773,10 +788,11 @@ class Message(Protocol): def buildReply(self, text=None): """ Builds and returns another message object with specified text. The to, - from and thread properties of new message are pre-set as reply to this - message + from, thread and type properties of new message are pre-set as reply to + this message """ - m = Message(to=self.getFrom(), frm=self.getTo(), body=text) + m = Message(to=self.getFrom(), frm=self.getTo(), body=text, + typ=self.getType()) th = self.getThread() if th: m.setThread(th) @@ -933,11 +949,20 @@ class Iq(Protocol): if queryNS: self.setQueryNS(queryNS) + def getQuery(self): + """ + Return the IQ's child element if it exists, None otherwise. + """ + children = self.getChildren() + if children and self.getType() != 'error' and \ + children[0].getName() != 'error': + return children[0] + def getQueryNS(self): """ Return the namespace of the 'query' child element """ - tag = self.getTag('query') + tag = self.getQuery() if tag: return tag.getNamespace() @@ -945,13 +970,15 @@ class Iq(Protocol): """ Return the 'node' attribute value of the 'query' child element """ - return self.getTagAttr('query', 'node') + tag = self.getQuery() + if tag: + return tag.getAttr('node') def getQueryPayload(self): """ Return the 'query' child element payload """ - tag = self.getTag('query') + tag = self.getQuery() if tag: return tag.getPayload() @@ -959,38 +986,79 @@ class Iq(Protocol): """ Return the 'query' child element child nodes """ - tag = self.getTag('query') + tag = self.getQuery() if tag: return tag.getChildren() + def setQuery(self, name=None): + """ + Change the name of the query node, creating it if needed. Keep the + existing name if none is given (use 'query' if it's a creation). + Return 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) + self.setQuery().setNamespace(namespace) def setQueryPayload(self, payload): """ Set the 'query' child element payload """ - self.setTag('query').setPayload(payload) + self.setQuery().setPayload(payload) def setQuerynode(self, node): """ Set the 'node' attribute value of the 'query' child element """ - self.setTagAttr('query', 'node', node) + self.setQuery().setAttr('node', node) def buildReply(self, typ): """ Build and return 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()) + iq = Iq(typ, to=self.getFrom(), frm=self.getTo(), + attrs={'id': self.getID()}) + iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS()) return iq +class Acks(Node): + """ + Acknowledgement elements for Stream Management + """ + def __init__(self, nsp=NS_STREAM_MGMT): + Node.__init__(self, None, {}, [], None, None,False, None) + self.setNamespace(nsp) + + def buildAnswer(self, handled): + """ + handled is the number of stanzas handled + """ + self.setName('a') + self.setAttr('h', handled) + + def buildRequest(self): + self.setName('r') + + def buildEnable(self, resume=False): + self.setName('enable') + if resume: + self.setAttr('resume', 'true') + + def buildResume(self, handled, previd): + self.setName('resume') + self.setAttr('h', handled) + self.setAttr('previd', previd) + class ErrorNode(Node): """ XMPP-style error element diff --git a/src/common/xmpp/roster_nb.py b/src/common/xmpp/roster_nb.py index c5a7bb463..9f520e3d3 100644 --- a/src/common/xmpp/roster_nb.py +++ b/src/common/xmpp/roster_nb.py @@ -45,7 +45,7 @@ class NonBlockingRoster(PlugIn): PlugIn.__init__(self) self.version = version self._data = {} - self.set=None + self._set=None self._exported_methods=[self.getRoster] self.received_from_server = False @@ -54,8 +54,8 @@ class NonBlockingRoster(PlugIn): 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 + if self._set is None: + self._set = 0 elif not force: return @@ -100,7 +100,7 @@ class NonBlockingRoster(PlugIn): if group.getData() not in self._data[jid]['groups']: self._data[jid]['groups'].append(group.getData()) self._data[self._owner.User+'@'+self._owner.Server]={'resources': {}, 'name': None, 'ask': None, 'subscription': None, 'groups': None,} - self.set=1 + self._set=1 # Looks like we have a workaround # raise NodeProcessed # a MUST. Otherwise you'll get back an <iq type='error'/> @@ -323,7 +323,7 @@ class NonBlockingRoster(PlugIn): 'subscription': None, 'groups': None } - self.set = 1 + self._set = 1 def plugin(self, owner, request=1): """ @@ -340,9 +340,9 @@ class NonBlockingRoster(PlugIn): def _on_roster_set(self, data): if data: self._owner.Dispatcher.ProcessNonBlocking(data) - if not self.set: + if not self._set: return - if not self._owner: + if not hasattr(self, '_owner') or not self._owner: # Connection has been closed by receiving a <stream:error> for ex, return self._owner.onreceive(None) @@ -356,7 +356,7 @@ class NonBlockingRoster(PlugIn): Request roster from server if neccessary and returns self """ return_self = True - if not self.set: + if not self._set: self.on_ready = on_ready self._owner.onreceive(self._on_roster_set) return_self = False diff --git a/src/common/xmpp/smacks.py b/src/common/xmpp/smacks.py new file mode 100644 index 000000000..e59b5d1b8 --- /dev/null +++ b/src/common/xmpp/smacks.py @@ -0,0 +1,130 @@ +from protocol import Acks +from protocol import NS_STREAM_MGMT +import logging +log = logging.getLogger('gajim.c.x.smacks') + +class Smacks(): + ''' + This is Smacks is the Stream Management class. It takes care of requesting + and sending acks. Also, it keeps track of the unhandled outgoing stanzas. + + The dispatcher has to be able to access this class to increment the + number of handled stanzas + ''' + + def __init__(self, con): + self.con = con # Connection object + self.out_h = 0 # Outgoing stanzas handled + self.in_h = 0 # Incoming stanzas handled + self.uqueue = [] # Unhandled stanzas queue + self.session_id = None + self.resumption = False # If server supports resume + # Max number of stanzas in queue before making a request + self.max_queue = 5 + self._owner = None + self.resuming = False + self.enabled = False # If SM is enabled + self.location = None + + def set_owner(self, owner): + self._owner = owner + + # Register handlers + owner.Dispatcher.RegisterNamespace(NS_STREAM_MGMT) + owner.Dispatcher.RegisterHandler('enabled', self._neg_response, + xmlns=NS_STREAM_MGMT) + owner.Dispatcher.RegisterHandler('r', self.send_ack, + xmlns=NS_STREAM_MGMT) + owner.Dispatcher.RegisterHandler('a', self.check_ack, + xmlns=NS_STREAM_MGMT) + owner.Dispatcher.RegisterHandler('resumed', self.check_ack, + xmlns=NS_STREAM_MGMT) + owner.Dispatcher.RegisterHandler('failed', self.error_handling, + xmlns=NS_STREAM_MGMT) + + def _neg_response(self, disp, stanza): + r = stanza.getAttr('resume') + if r == 'true' or r == 'True' or r == '1': + self.resumption = True + self.session_id = stanza.getAttr('id') + + if r == 'false' or r == 'False' or r == '0': + self.negociate(False) + + l = stanza.getAttr('location') + if l: + self.location = l + + def negociate(self, resume=True): + # Every time we attempt to negociate, we must erase all previous info + # about any previous session + self.uqueue = [] + self.in_h = 0 + self.out_h = 0 + self.session_id = None + self.enabled = True + + stanza = Acks() + stanza.buildEnable(resume) + self._owner.Connection.send(stanza, now=True) + + def resume_request(self): + if not self.session_id: + self.resuming = False + log.error('Attempted to resume without a valid session id ') + return + resume = Acks() + resume.buildResume(self.in_h, self.session_id) + self._owner.Connection.send(resume, False) + + def send_ack(self, disp, stanza): + ack = Acks() + ack.buildAnswer(self.in_h) + self._owner.Connection.send(ack, False) + + def request_ack(self): + r = Acks() + r.buildRequest() + self._owner.Connection.send(r, False) + + def check_ack(self, disp, stanza): + ''' + Checks if the number of stanzas sent are the same as the + number of stanzas received by the server. Pops stanzas that were + handled by the server from the queue. + ''' + h = int(stanza.getAttr('h')) + diff = self.out_h - h + + if len(self.uqueue) < diff or diff < 0: + log.error('Server and client number of stanzas handled mismatch ') + else: + while (len(self.uqueue) > diff): + self.uqueue.pop(0) + + if stanza.getName() == 'resumed': + self.resuming = True + self.con.set_oldst() + if self.uqueue != []: + for i in self.uqueue: + self._owner.Connection.send(i, False) + + def error_handling(self, disp, stanza): + # If the server doesn't recognize previd, forget about resuming + # Ask for service discovery, etc.. + if stanza.getTag('item-not-found'): + self.resuming = False + # we need to bind a resource + self._owner.NonBlockingBind.resuming = False + self._owner._on_auth_bind(None) + return + + # Doesn't support resumption + if stanza.getTag('feature-not-implemented'): + self.negociate(False) + return + + if stanza.getTag('unexpected-request'): + self.enabled = False + log.error('Gajim failed to negociate Stream Management') + return diff --git a/src/common/xmpp/stringprepare.py b/src/common/xmpp/stringprepare.py index bae16fcec..657895c84 100644 --- a/src/common/xmpp/stringprepare.py +++ b/src/common/xmpp/stringprepare.py @@ -2,7 +2,7 @@ ## src/common/xmpp/stringprepare.py ## ## Copyright (C) 2001-2005 Twisted Matrix Laboratories -## Copyright (C) 2005-2010 Yann Leboulanger <asterix AT lagaule.org> +## Copyright (C) 2005-2011 Yann Leboulanger <asterix AT lagaule.org> ## Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de> ## Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org> ## @@ -202,6 +202,8 @@ class NamePrep: def nameprep(self, label): label = idna.nameprep(label) self.check_prohibiteds(label) + if len(label) == 0: + raise UnicodeError, "Invalid empty name" if label[0] == '-': raise UnicodeError, "Invalid leading hyphen-minus" if label[-1] == '-': diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 7a9c80f98..cc8393d52 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -359,7 +359,8 @@ class NonBlockingTLS(PlugIn): tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) log.debug('Using client cert and key from %s' % conn.client_cert) try: - p12 = OpenSSL.crypto.load_pkcs12(open(conn.client_cert).read()) + p12 = OpenSSL.crypto.load_pkcs12(open(conn.client_cert).read(), + conn.client_cert_passphrase) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load client pkcs12 certificate from ' 'file %s: %s ... Is it a valid PKCS12 cert?' % \ |